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