1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct JwtIntrospectionClaims {
16 pub iss: String,
18
19 pub aud: Vec<String>,
21
22 pub jti: String,
24
25 pub iat: i64,
27
28 pub exp: i64,
30
31 pub sub: Option<String>,
33
34 pub client_id: Option<String>,
36
37 pub active: bool,
39
40 pub token_type: Option<String>,
42
43 pub scope: Option<String>,
45
46 pub username: Option<String>,
48
49 pub token_exp: Option<i64>,
51
52 pub token_iat: Option<i64>,
54
55 pub token_nbf: Option<i64>,
57
58 pub token_aud: Option<Vec<String>>,
60
61 pub token_iss: Option<String>,
63
64 #[serde(flatten)]
66 pub additional_claims: HashMap<String, Value>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BasicIntrospectionResponse {
72 pub active: bool,
74
75 pub scope: Option<String>,
77
78 pub client_id: Option<String>,
80
81 pub username: Option<String>,
83
84 pub token_type: Option<String>,
86
87 pub exp: Option<i64>,
89
90 pub iat: Option<i64>,
92
93 pub nbf: Option<i64>,
95
96 pub sub: Option<String>,
98
99 pub aud: Option<Vec<String>>,
101
102 pub iss: Option<String>,
104
105 pub jti: Option<String>,
107
108 #[serde(flatten)]
110 pub additional_claims: HashMap<String, Value>,
111}
112
113#[derive(Debug, Clone)]
115pub struct JwtIntrospectionConfig {
116 pub issuer: String,
118
119 pub default_audience: Vec<String>,
121
122 pub response_expiration: i64,
124
125 pub signing_algorithm: Algorithm,
127
128 pub include_token_claims: bool,
130
131 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, signing_algorithm: Algorithm::HS256,
142 include_token_claims: true,
143 validate_audience: true,
144 }
145 }
146}
147
148pub struct JwtIntrospectionManager {
150 config: JwtIntrospectionConfig,
151 private_key: EncodingKey,
152 public_key: DecodingKey,
153}
154
155impl JwtIntrospectionManager {
156 pub fn new(config: JwtIntrospectionConfig) -> Result<Self> {
158 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 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 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 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 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 pub fn validate_request_audience(&self, requested_audience: &[String]) -> bool {
287 if !self.config.validate_audience {
288 return true;
289 }
290
291 requested_audience
293 .iter()
294 .any(|aud| self.config.default_audience.contains(aud))
295 }
296
297 pub fn get_issuer(&self) -> &str {
299 &self.config.issuer
300 }
301
302 pub fn get_default_audience(&self) -> &[String] {
304 &self.config.default_audience
305 }
306
307 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 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); }
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