1use crate::errors::{AuthError, Result};
8use crate::server::jwt::jwt_access_tokens::JwtAccessTokenValidator;
9use crate::storage::AuthStorage;
10use crate::tokens::{AuthToken, TokenManager};
11use base64::{Engine as _, engine::general_purpose};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct TokenIntrospectionRequest {
20 pub token: String,
22
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub token_type_hint: Option<String>,
26
27 #[serde(flatten)]
29 pub additional_params: HashMap<String, String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TokenIntrospectionResponse {
35 pub active: bool,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub scope: Option<String>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub client_id: Option<String>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub username: Option<String>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub token_type: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub exp: Option<i64>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub iat: Option<i64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub nbf: Option<i64>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub sub: Option<String>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub aud: Option<Vec<String>>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub iss: Option<String>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub jti: Option<String>,
81
82 #[serde(flatten)]
84 pub additional_attributes: HashMap<String, serde_json::Value>,
85}
86
87impl TokenIntrospectionResponse {
88 pub fn inactive() -> Self {
90 Self {
91 active: false,
92 scope: None,
93 client_id: None,
94 username: None,
95 token_type: None,
96 exp: None,
97 iat: None,
98 nbf: None,
99 sub: None,
100 aud: None,
101 iss: None,
102 jti: None,
103 additional_attributes: HashMap::new(),
104 }
105 }
106
107 pub fn from_auth_token(
109 token: &AuthToken,
110 client_id: Option<String>,
111 issuer: Option<String>,
112 ) -> Self {
113 Self {
114 active: !token.is_expired(),
115 scope: if token.scopes.is_empty() {
116 None
117 } else {
118 Some(token.scopes.join(" "))
119 },
120 client_id,
121 username: Some(token.user_id.clone()),
122 token_type: Some("Bearer".to_string()),
123 exp: Some(token.expires_at.timestamp()),
124 iat: Some(token.issued_at.timestamp()),
125 nbf: Some(token.issued_at.timestamp()),
126 sub: Some(token.user_id.clone()),
127 aud: None, iss: issuer,
129 jti: Some(token.token_id.clone()),
130 additional_attributes: HashMap::new(),
131 }
132 }
133}
134
135#[derive(Debug, Clone)]
137pub struct TokenIntrospectionConfig {
138 pub enabled: bool,
140
141 pub issuer: String,
143
144 pub include_detailed_info: bool,
146
147 pub rate_limit_per_minute: u32,
149
150 pub supported_token_types: Vec<String>,
152
153 pub require_client_authentication: bool,
155}
156
157impl Default for TokenIntrospectionConfig {
158 fn default() -> Self {
159 Self {
160 enabled: true,
161 issuer: "https://auth.example.com".to_string(),
162 include_detailed_info: true,
163 rate_limit_per_minute: 100,
164 supported_token_types: vec!["access_token".to_string(), "refresh_token".to_string()],
165 require_client_authentication: true,
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct IntrospectionClientCredentials {
173 pub client_id: String,
175
176 pub client_secret: Option<String>,
178
179 pub client_assertion: Option<String>,
181
182 pub auth_method: ClientAuthMethod,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum ClientAuthMethod {
189 ClientSecretBasic,
191
192 ClientSecretPost,
194
195 ClientSecretJwt,
197
198 PrivateKeyJwt,
200
201 None,
203}
204
205pub struct TokenIntrospectionService {
207 config: TokenIntrospectionConfig,
209
210 storage: Arc<dyn AuthStorage>,
212
213 token_manager: Arc<TokenManager>,
215
216 jwt_validator: Option<JwtAccessTokenValidator>,
218
219 rate_limiter: Arc<tokio::sync::RwLock<HashMap<String, Vec<DateTime<Utc>>>>>,
221}
222
223impl TokenIntrospectionService {
224 pub fn new(
226 config: TokenIntrospectionConfig,
227 storage: Arc<dyn AuthStorage>,
228 token_manager: Arc<TokenManager>,
229 jwt_validator: Option<JwtAccessTokenValidator>,
230 ) -> Self {
231 Self {
232 config,
233 storage,
234 token_manager,
235 jwt_validator,
236 rate_limiter: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
237 }
238 }
239
240 pub async fn introspect_token(
242 &self,
243 request: TokenIntrospectionRequest,
244 client_credentials: Option<IntrospectionClientCredentials>,
245 ) -> Result<TokenIntrospectionResponse> {
246 if !self.config.enabled {
248 return Err(AuthError::access_denied("Token introspection is disabled"));
249 }
250
251 if self.config.require_client_authentication {
253 let credentials = client_credentials.ok_or_else(|| {
254 AuthError::access_denied("Client authentication required for token introspection")
255 })?;
256
257 self.validate_client_credentials(&credentials).await?;
258
259 self.check_rate_limit(&credentials.client_id).await?;
261 }
262
263 let token_type = request.token_type_hint.as_deref().unwrap_or("access_token");
265
266 match token_type {
267 "access_token" => self.introspect_access_token(&request.token).await,
268 "refresh_token" => self.introspect_refresh_token(&request.token).await,
269 _ => self.introspect_unknown_token(&request.token).await,
270 }
271 }
272
273 async fn introspect_access_token(&self, token: &str) -> Result<TokenIntrospectionResponse> {
275 if let Some(ref jwt_validator) = self.jwt_validator
277 && let Ok(claims) = jwt_validator.validate_jwt_access_token(token)
278 {
279 return Ok(TokenIntrospectionResponse {
280 active: true,
281 scope: claims.scope,
282 client_id: Some(claims.client_id),
283 username: Some(claims.sub.clone()),
284 token_type: Some("Bearer".to_string()),
285 exp: Some(claims.exp),
286 iat: Some(claims.iat),
287 nbf: claims.nbf,
288 sub: Some(claims.sub),
289 aud: Some(claims.aud),
290 iss: Some(claims.iss),
291 jti: Some(claims.jti),
292 additional_attributes: HashMap::new(),
293 });
294 }
295
296 match self.storage.get_token(token).await? {
298 Some(auth_token) => {
299 if auth_token.is_expired() {
300 Ok(TokenIntrospectionResponse::inactive())
301 } else {
302 Ok(TokenIntrospectionResponse::from_auth_token(
303 &auth_token,
304 None, Some(self.config.issuer.clone()),
306 ))
307 }
308 }
309 None => Ok(TokenIntrospectionResponse::inactive()),
310 }
311 }
312
313 async fn introspect_refresh_token(&self, token: &str) -> Result<TokenIntrospectionResponse> {
315 match self.storage.get_token(token).await? {
318 Some(auth_token) => {
319 if let Some(ref refresh_token) = auth_token.refresh_token {
320 if refresh_token == token && !auth_token.is_expired() {
321 let mut response = TokenIntrospectionResponse::from_auth_token(
322 &auth_token,
323 None,
324 Some(self.config.issuer.clone()),
325 );
326 response.token_type = Some("refresh_token".to_string());
327 Ok(response)
328 } else {
329 Ok(TokenIntrospectionResponse::inactive())
330 }
331 } else {
332 Ok(TokenIntrospectionResponse::inactive())
333 }
334 }
335 None => Ok(TokenIntrospectionResponse::inactive()),
336 }
337 }
338
339 async fn introspect_unknown_token(&self, token: &str) -> Result<TokenIntrospectionResponse> {
341 let access_result = self.introspect_access_token(token).await?;
343 if access_result.active {
344 return Ok(access_result);
345 }
346
347 self.introspect_refresh_token(token).await
348 }
349
350 async fn validate_client_credentials(
352 &self,
353 credentials: &IntrospectionClientCredentials,
354 ) -> Result<()> {
355 if credentials.client_id.is_empty() {
356 return Err(AuthError::access_denied("Invalid client_id"));
357 }
358
359 match credentials.auth_method {
360 ClientAuthMethod::ClientSecretBasic | ClientAuthMethod::ClientSecretPost => {
361 if let Some(client_secret) = &credentials.client_secret {
362 let client_key = format!("oauth_client:{}", credentials.client_id);
364 if let Some(client_data) = self.storage.get_kv(&client_key).await? {
365 let client_str = std::str::from_utf8(&client_data).map_err(|e| {
366 AuthError::internal(format!("Invalid UTF-8 in client data: {}", e))
367 })?;
368 let client: serde_json::Value =
369 serde_json::from_str(client_str).map_err(|e| {
370 AuthError::internal(format!("Failed to deserialize client: {}", e))
371 })?;
372
373 if let Some(stored_secret) =
374 client.get("client_secret").and_then(|v| v.as_str())
375 {
376 if !crate::security::secure_utils::constant_time_compare(
377 client_secret.as_bytes(),
378 stored_secret.as_bytes(),
379 ) {
380 return Err(AuthError::access_denied("Invalid client secret"));
381 }
382 } else {
383 return Err(AuthError::access_denied("Client secret not found"));
384 }
385 } else {
386 return Err(AuthError::access_denied("Client not found"));
387 }
388 } else {
389 return Err(AuthError::access_denied("Client secret required"));
390 }
391 }
392 ClientAuthMethod::ClientSecretJwt | ClientAuthMethod::PrivateKeyJwt => {
393 if let Some(client_assertion) = &credentials.client_assertion {
394 if let Ok(claims) = self.token_manager.validate_jwt_token(client_assertion) {
396 if claims.sub != credentials.client_id {
397 return Err(AuthError::access_denied(
398 "JWT subject doesn't match client_id",
399 ));
400 }
401 if claims.aud.is_empty() || !claims.aud.contains(&self.config.issuer) {
402 return Err(AuthError::access_denied("Invalid JWT audience"));
403 }
404 } else {
405 return Err(AuthError::access_denied("Invalid JWT assertion"));
406 }
407 } else {
408 return Err(AuthError::access_denied(
409 "Client assertion required for JWT auth",
410 ));
411 }
412 }
413 ClientAuthMethod::None => {
414 }
416 }
417
418 Ok(())
419 }
420
421 async fn check_rate_limit(&self, client_id: &str) -> Result<()> {
423 let mut rate_limiter = self.rate_limiter.write().await;
424 let now = Utc::now();
425 let one_minute_ago = now - chrono::Duration::minutes(1);
426
427 let requests = rate_limiter
429 .entry(client_id.to_string())
430 .or_insert_with(Vec::new);
431 requests.retain(|×tamp| timestamp > one_minute_ago);
432
433 if requests.len() >= self.config.rate_limit_per_minute as usize {
434 return Err(AuthError::access_denied(
435 "Rate limit exceeded for token introspection",
436 ));
437 }
438
439 requests.push(now);
440 Ok(())
441 }
442
443 pub fn get_metadata(&self) -> HashMap<String, serde_json::Value> {
445 let mut metadata = HashMap::new();
446
447 metadata.insert(
448 "introspection_endpoint".to_string(),
449 serde_json::Value::String(format!("{}/introspect", self.config.issuer)),
450 );
451
452 metadata.insert(
453 "introspection_endpoint_auth_methods_supported".to_string(),
454 serde_json::Value::Array(vec![
455 serde_json::Value::String("client_secret_basic".to_string()),
456 serde_json::Value::String("client_secret_post".to_string()),
457 ]),
458 );
459
460 metadata.insert(
461 "token_introspection_supported".to_string(),
462 serde_json::Value::Bool(self.config.enabled),
463 );
464
465 metadata
466 }
467}
468
469pub struct TokenIntrospectionHandler {
471 service: Arc<TokenIntrospectionService>,
473}
474
475impl TokenIntrospectionHandler {
476 pub fn new(service: Arc<TokenIntrospectionService>) -> Self {
478 Self { service }
479 }
480
481 pub async fn handle_introspection_request(
483 &self,
484 request_body: &str,
485 authorization_header: Option<&str>,
486 ) -> Result<String> {
487 let request = self.parse_introspection_request(request_body)?;
489
490 let client_credentials =
492 self.extract_client_credentials(authorization_header, request_body)?;
493
494 let response = self
496 .service
497 .introspect_token(request, client_credentials)
498 .await?;
499
500 serde_json::to_string(&response).map_err(|e| {
502 AuthError::internal(format!("Failed to serialize introspection response: {}", e))
503 })
504 }
505
506 fn parse_introspection_request(&self, body: &str) -> Result<TokenIntrospectionRequest> {
508 let mut token = None;
509 let mut token_type_hint = None;
510 let mut additional_params = HashMap::new();
511
512 for pair in body.split('&') {
513 if let Some((key, value)) = pair.split_once('=') {
514 let key = urlencoding::decode(key).map_err(|e| {
515 AuthError::validation(format!("Invalid URL encoding in key: {}", e))
516 })?;
517 let value = urlencoding::decode(value).map_err(|e| {
518 AuthError::validation(format!("Invalid URL encoding in value: {}", e))
519 })?;
520
521 match key.as_ref() {
522 "token" => token = Some(value.to_string()),
523 "token_type_hint" => token_type_hint = Some(value.to_string()),
524 _ => {
525 additional_params.insert(key.to_string(), value.to_string());
526 }
527 }
528 }
529 }
530
531 let token =
532 token.ok_or_else(|| AuthError::validation("Missing required parameter: token"))?;
533
534 Ok(TokenIntrospectionRequest {
535 token,
536 token_type_hint,
537 additional_params,
538 })
539 }
540
541 fn extract_client_credentials(
543 &self,
544 authorization_header: Option<&str>,
545 request_body: &str,
546 ) -> Result<Option<IntrospectionClientCredentials>> {
547 if let Some(auth_header) = authorization_header
549 && let Some(encoded) = auth_header.strip_prefix("Basic ")
550 {
551 let decoded = general_purpose::STANDARD.decode(encoded).map_err(|e| {
552 AuthError::validation(format!("Invalid Basic auth encoding: {}", e))
553 })?;
554 let credentials = String::from_utf8(decoded)
555 .map_err(|e| AuthError::validation(format!("Invalid Basic auth UTF-8: {}", e)))?;
556
557 if let Some((client_id, client_secret)) = credentials.split_once(':') {
558 return Ok(Some(IntrospectionClientCredentials {
559 client_id: client_id.to_string(),
560 client_secret: Some(client_secret.to_string()),
561 client_assertion: None,
562 auth_method: ClientAuthMethod::ClientSecretBasic,
563 }));
564 }
565 }
566
567 let mut client_id = None;
569 let mut client_secret = None;
570
571 for pair in request_body.split('&') {
572 if let Some((key, value)) = pair.split_once('=') {
573 let key = urlencoding::decode(key).unwrap_or_default();
574 let value = urlencoding::decode(value).unwrap_or_default();
575
576 match key.as_ref() {
577 "client_id" => client_id = Some(value.to_string()),
578 "client_secret" => client_secret = Some(value.to_string()),
579 _ => {}
580 }
581 }
582 }
583
584 if let Some(client_id) = client_id {
585 return Ok(Some(IntrospectionClientCredentials {
586 client_id,
587 client_secret,
588 client_assertion: None,
589 auth_method: ClientAuthMethod::ClientSecretPost,
590 }));
591 }
592
593 Ok(None)
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use crate::testing::MockStorage;
601 use crate::tokens::TokenManager;
602 use chrono::Duration;
603
604 fn create_test_service() -> TokenIntrospectionService {
605 let config = TokenIntrospectionConfig::default();
606 let storage = Arc::new(MockStorage::new());
607 let secret = b"test-secret-key-32-bytes-minimum!";
608 let token_manager = Arc::new(TokenManager::new_hmac(
609 secret,
610 "test-issuer",
611 "test-audience",
612 ));
613
614 TokenIntrospectionService::new(config, storage, token_manager, None)
615 }
616
617 #[tokio::test]
618 async fn test_inactive_token_introspection() {
619 let service = create_test_service();
620
621 let client_data = serde_json::json!({
623 "client_id": "test-client",
624 "client_secret": "test-secret"
625 });
626 service
627 .storage
628 .store_kv(
629 "oauth_client:test-client",
630 client_data.to_string().as_bytes(),
631 None,
632 )
633 .await
634 .unwrap();
635
636 let client_credentials = IntrospectionClientCredentials {
638 client_id: "test-client".to_string(),
639 client_secret: Some("test-secret".to_string()),
640 client_assertion: None,
641 auth_method: ClientAuthMethod::ClientSecretBasic,
642 };
643
644 let request = TokenIntrospectionRequest {
645 token: "invalid-token".to_string(),
646 token_type_hint: Some("access_token".to_string()),
647 additional_params: HashMap::new(),
648 };
649
650 let response = service
651 .introspect_token(request, Some(client_credentials))
652 .await
653 .unwrap();
654 assert!(!response.active);
655 }
656
657 #[tokio::test]
658 async fn test_client_credentials_validation() {
659 let service = create_test_service();
660
661 let client_data = serde_json::json!({
663 "client_id": "test-client",
664 "client_secret": "test-secret"
665 });
666 service
667 .storage
668 .store_kv(
669 "oauth_client:test-client",
670 client_data.to_string().as_bytes(),
671 None,
672 )
673 .await
674 .unwrap();
675
676 let valid_credentials = IntrospectionClientCredentials {
677 client_id: "test-client".to_string(),
678 client_secret: Some("test-secret".to_string()),
679 client_assertion: None,
680 auth_method: ClientAuthMethod::ClientSecretBasic,
681 };
682
683 assert!(
685 service
686 .validate_client_credentials(&valid_credentials)
687 .await
688 .is_ok()
689 );
690
691 let invalid_credentials = IntrospectionClientCredentials {
692 client_id: "".to_string(),
693 client_secret: None,
694 client_assertion: None,
695 auth_method: ClientAuthMethod::ClientSecretBasic,
696 };
697
698 assert!(
700 service
701 .validate_client_credentials(&invalid_credentials)
702 .await
703 .is_err()
704 );
705 }
706
707 #[tokio::test]
708 async fn test_rate_limiting() {
709 let service = create_test_service();
710 let client_id = "test-client";
711
712 for _ in 0..10 {
714 assert!(service.check_rate_limit(client_id).await.is_ok());
715 }
716
717 for _ in 0..service.config.rate_limit_per_minute {
719 let _ = service.check_rate_limit(client_id).await;
720 }
721
722 assert!(service.check_rate_limit(client_id).await.is_err());
724 }
725
726 #[tokio::test]
727 async fn test_token_introspection_response_creation() {
728 let token = AuthToken {
729 token_id: "test-token".to_string(),
730 user_id: "test-user".to_string(),
731 access_token: "test-access-token".to_string(),
732 token_type: Some("Bearer".to_string()),
733 subject: None,
734 issuer: None,
735 refresh_token: None,
736 issued_at: Utc::now(),
737 expires_at: Utc::now() + Duration::hours(1),
738 scopes: vec!["read".to_string(), "write".to_string()],
739 auth_method: "test".to_string(),
740 client_id: None,
741 user_profile: None,
742 permissions: vec!["read:data".to_string(), "write:data".to_string()],
743 roles: vec!["user".to_string()],
744 metadata: Default::default(),
745 };
746
747 let response = TokenIntrospectionResponse::from_auth_token(
748 &token,
749 Some("test-client".to_string()),
750 Some("https://auth.example.com".to_string()),
751 );
752
753 assert!(response.active);
754 assert_eq!(response.client_id.unwrap(), "test-client");
755 assert_eq!(response.username.unwrap(), "test-user");
756 assert_eq!(response.scope.unwrap(), "read write");
757 assert_eq!(response.token_type.unwrap(), "Bearer");
758 assert_eq!(response.iss.unwrap(), "https://auth.example.com");
759 }
760
761 #[test]
762 fn test_introspection_handler_request_parsing() {
763 let service = create_test_service();
764 let handler = TokenIntrospectionHandler::new(Arc::new(service));
765
766 let request_body = "token=test-token&token_type_hint=access_token";
767 let request = handler.parse_introspection_request(request_body).unwrap();
768
769 assert_eq!(request.token, "test-token");
770 assert_eq!(request.token_type_hint.unwrap(), "access_token");
771 }
772
773 #[test]
774 fn test_client_credentials_extraction() {
775 let service = create_test_service();
776 let handler = TokenIntrospectionHandler::new(Arc::new(service));
777
778 let auth_header = "Basic dGVzdC1jbGllbnQ6dGVzdC1zZWNyZXQ="; let credentials = handler
781 .extract_client_credentials(Some(auth_header), "")
782 .unwrap()
783 .unwrap();
784
785 assert_eq!(credentials.client_id, "test-client");
786 assert_eq!(credentials.client_secret.unwrap(), "test-secret");
787 assert_eq!(credentials.auth_method, ClientAuthMethod::ClientSecretBasic);
788
789 let request_body = "token=test&client_id=test-client&client_secret=test-secret";
791 let credentials = handler
792 .extract_client_credentials(None, request_body)
793 .unwrap()
794 .unwrap();
795
796 assert_eq!(credentials.client_id, "test-client");
797 assert_eq!(credentials.client_secret.unwrap(), "test-secret");
798 assert_eq!(credentials.auth_method, ClientAuthMethod::ClientSecretPost);
799 }
800
801 #[test]
802 fn test_metadata_generation() {
803 let service = create_test_service();
804 let metadata = service.get_metadata();
805
806 assert!(metadata.contains_key("introspection_endpoint"));
807 assert!(metadata.contains_key("introspection_endpoint_auth_methods_supported"));
808 assert!(metadata.contains_key("token_introspection_supported"));
809 }
810}
811
812