Skip to main content

auth_framework/server/token_exchange/
token_introspection.rs

1//! OAuth 2.0 Token Introspection (RFC 7662)
2//!
3//! This module implements RFC 7662, which defines a method for a protected resource
4//! to query an OAuth 2.0 authorization server to determine meta-information about
5//! an OAuth 2.0 token.
6
7use 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/// Token introspection request (RFC 7662)
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TokenIntrospectionRequest {
21    /// The string value of the token
22    pub token: String,
23
24    /// Optional hint about the type of token being introspected
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub token_type_hint: Option<String>,
27
28    /// Additional parameters for extension specifications
29    #[serde(flatten)]
30    pub additional_params: HashMap<String, String>,
31}
32
33/// Token introspection response (RFC 7662)
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TokenIntrospectionResponse {
36    /// Boolean indicator of whether the token is currently active
37    pub active: bool,
38
39    /// Space-separated list of scopes associated with the token
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub scope: Option<String>,
42
43    /// Client identifier for the OAuth 2.0 client
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub client_id: Option<String>,
46
47    /// Human-readable identifier for the resource owner
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub username: Option<String>,
50
51    /// Type of the token (e.g., "Bearer")
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub token_type: Option<String>,
54
55    /// Integer timestamp of when the token expires
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub exp: Option<i64>,
58
59    /// Integer timestamp of when the token was issued
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub iat: Option<i64>,
62
63    /// Integer timestamp of when the token is not to be used before
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub nbf: Option<i64>,
66
67    /// Subject of the token (usually a machine-readable identifier)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub sub: Option<String>,
70
71    /// Intended audience for the token
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub aud: Option<Vec<String>>,
74
75    /// Issuer of the token
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub iss: Option<String>,
78
79    /// Unique identifier for the token
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub jti: Option<String>,
82
83    /// Additional token attributes
84    #[serde(flatten)]
85    pub additional_attributes: HashMap<String, serde_json::Value>,
86}
87
88impl TokenIntrospectionResponse {
89    /// Create an inactive token response
90    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    /// Create an active token response from AuthToken
109    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, // Set based on configuration
129            iss: issuer,
130            jti: Some(token.token_id.clone()),
131            additional_attributes: HashMap::new(),
132        }
133    }
134}
135
136/// Token introspection endpoint configuration
137#[derive(Debug, Clone)]
138pub struct TokenIntrospectionConfig {
139    /// Whether introspection is enabled
140    pub enabled: bool,
141
142    /// Issuer identifier
143    pub issuer: String,
144
145    /// Whether to include detailed token information
146    pub include_detailed_info: bool,
147
148    /// Maximum number of introspection requests per client per minute
149    pub rate_limit_per_minute: u32,
150
151    /// Supported token types for introspection
152    pub supported_token_types: Vec<String>,
153
154    /// Whether to validate client credentials for introspection
155    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/// Token introspection client credentials
172#[derive(Debug, Clone)]
173pub struct IntrospectionClientCredentials {
174    /// Client identifier
175    pub client_id: String,
176
177    /// Client secret (if required)
178    pub client_secret: Option<String>,
179
180    /// Client assertion for JWT authentication
181    pub client_assertion: Option<String>,
182
183    /// Client authentication method
184    pub auth_method: ClientAuthMethod,
185}
186
187/// Client authentication methods for introspection
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum ClientAuthMethod {
190    /// HTTP Basic authentication with client_id and client_secret
191    ClientSecretBasic,
192
193    /// POST body authentication with client_id and client_secret
194    ClientSecretPost,
195
196    /// JWT-based client authentication
197    ClientSecretJwt,
198
199    /// Private key JWT authentication
200    PrivateKeyJwt,
201
202    /// No authentication (public clients)
203    None,
204}
205
206/// Token introspection service
207pub struct TokenIntrospectionService {
208    /// Configuration
209    config: TokenIntrospectionConfig,
210
211    /// Storage backend
212    storage: Arc<dyn AuthStorage>,
213
214    /// Token manager for JWT validation
215    token_manager: Arc<TokenManager>,
216
217    /// JWT access token validator
218    jwt_validator: Option<JwtAccessTokenValidator>,
219
220    /// Rate limiting tracker
221    rate_limiter: Arc<tokio::sync::RwLock<HashMap<String, Vec<DateTime<Utc>>>>>,
222}
223
224impl TokenIntrospectionService {
225    /// Create a new token introspection service
226    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    /// Handle token introspection request
242    pub async fn introspect_token(
243        &self,
244        request: TokenIntrospectionRequest,
245        client_credentials: Option<IntrospectionClientCredentials>,
246    ) -> Result<TokenIntrospectionResponse> {
247        // Check if introspection is enabled
248        if !self.config.enabled {
249            return Err(AuthError::access_denied("Token introspection is disabled"));
250        }
251
252        // Validate client credentials if required
253        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            // Check rate limiting
261            self.check_rate_limit(&credentials.client_id).await?;
262        }
263
264        // Determine token type and introspect accordingly
265        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    /// Introspect an access token
275    async fn introspect_access_token(&self, token: &str) -> Result<TokenIntrospectionResponse> {
276        // Try JWT access token first if validator is available
277        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        // Try opaque token lookup in storage
298        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, // Would need client ID from token metadata
306                        Some(self.config.issuer.clone()),
307                    ))
308                }
309            }
310            None => Ok(TokenIntrospectionResponse::inactive()),
311        }
312    }
313
314    /// Introspect a refresh token
315    async fn introspect_refresh_token(&self, token: &str) -> Result<TokenIntrospectionResponse> {
316        // Look up refresh token in storage
317        // This would need to be implemented in the storage backend
318        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    /// Introspect an unknown token type
343    async fn introspect_unknown_token(&self, token: &str) -> Result<TokenIntrospectionResponse> {
344        // Try as access token first, then refresh token
345        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    /// Validate client credentials for introspection
354    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                    // Validate against client registry if available
366                    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                    // Validate JWT assertion
398                    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                // Public client - no validation needed
418            }
419        }
420
421        Ok(())
422    }
423
424    /// Check rate limiting for introspection requests
425    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        // Clean up old entries and count recent requests
431        let requests = rate_limiter
432            .entry(client_id.to_string())
433            .or_insert_with(Vec::new);
434        requests.retain(|&timestamp| 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    /// Get introspection endpoint metadata
447    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
472/// HTTP handler for token introspection endpoint
473pub struct TokenIntrospectionHandler {
474    /// Introspection service
475    service: Arc<TokenIntrospectionService>,
476}
477
478impl TokenIntrospectionHandler {
479    /// Create a new introspection handler
480    pub fn new(service: Arc<TokenIntrospectionService>) -> Self {
481        Self { service }
482    }
483
484    /// Handle HTTP POST request to introspection endpoint
485    pub async fn handle_introspection_request(
486        &self,
487        request_body: &str,
488        authorization_header: Option<&str>,
489    ) -> Result<String> {
490        // Parse request body (application/x-www-form-urlencoded)
491        let request = self.parse_introspection_request(request_body)?;
492
493        // Extract client credentials from Authorization header or request body
494        let client_credentials =
495            self.extract_client_credentials(authorization_header, request_body)?;
496
497        // Perform introspection
498        let response = self
499            .service
500            .introspect_token(request, client_credentials)
501            .await?;
502
503        // Serialize response as JSON
504        serde_json::to_string(&response).map_err(|e| {
505            AuthError::internal(format!("Failed to serialize introspection response: {}", e))
506        })
507    }
508
509    /// Parse introspection request from form data
510    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    /// Extract client credentials from Authorization header or request body
545    fn extract_client_credentials(
546        &self,
547        authorization_header: Option<&str>,
548        request_body: &str,
549    ) -> Result<Option<IntrospectionClientCredentials>> {
550        // Try Basic authentication first
551        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        // Try POST body credentials
571        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        // Register a test client first
625        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        // Provide client credentials for introspection
640        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        // Register the test client in storage first
665        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        // Should not error with valid credentials
687        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        // Should error with invalid credentials
702        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        // Should allow requests under the limit
716        for _ in 0..10 {
717            assert!(service.check_rate_limit(client_id).await.is_ok());
718        }
719
720        // Should deny requests over the limit
721        for _ in 0..service.config.rate_limit_per_minute {
722            let _ = service.check_rate_limit(client_id).await;
723        }
724
725        // This should be rate limited
726        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        // Test Basic authentication
782        let auth_header = "Basic dGVzdC1jbGllbnQ6dGVzdC1zZWNyZXQ="; // test-client:test-secret
783        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        // Test POST body authentication
793        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}