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