Skip to main content

rust_mcp_extra/token_verifier/
generic_token_verifier.rs

1use crate::token_verifier::jwt_cache::JwtCache;
2use async_lock::RwLock;
3use async_trait::async_trait;
4use reqwest::{header::AUTHORIZATION, StatusCode};
5use rust_mcp_sdk::{
6    auth::{
7        decode_token_header, Audience, AuthInfo, AuthenticationError, IntrospectionResponse,
8        JsonWebKeySet, OauthTokenVerifier,
9    },
10    mcp_http::error_message_from_response,
11};
12use serde_json::Value;
13use std::{
14    collections::HashMap,
15    time::{Duration, SystemTime},
16};
17use url::Url;
18
19const JWKS_REFRESH_TIME: Duration = Duration::from_secs(24 * 60 * 60); // re-fetch jwks every 24 hours
20const REMOTE_VERIFICATION_INTERVAL: Duration = Duration::from_secs(15 * 60); // 15 minutes
21const JWT_CACHE_CAPACITY: usize = 1000;
22
23struct JwksCache {
24    last_updated: Option<SystemTime>,
25    jwks: JsonWebKeySet,
26}
27
28/// Supported OAuth token verification strategies.
29///
30/// Each variant represents a different method for validating access tokens,
31/// depending on what the authorization server exposes or what your application
32/// requires.
33pub enum VerificationStrategies {
34    /// Verifies tokens by calling the authorization server's introspection
35    /// endpoint, as defined in RFC 7662.
36    ///
37    /// This method allows the resource server to validate opaque or JWT tokens
38    /// by sending them to the introspection URI along with its client credentials.
39    Introspection {
40        /// The OAuth introspection endpoint.
41        introspection_uri: String,
42        /// Client identifier used to authenticate the introspection request.
43        client_id: String,
44        /// Client secret used to authenticate the introspection request.
45        client_secret: String,
46        /// Indicates whether the OAuth2 client should use HTTP Basic Authentication when
47        ///calling the token introspection endpoint.
48        /// if false: client_id and client_secret will be sent in the POST body instead of using Basic Authentication
49        use_basic_auth: bool,
50        /// Optional key-value pairs to include as additional parameters in the
51        /// body of the token introspection request.
52        /// Example : ("token_type_hint", "access_token")
53        extra_params: Option<Vec<(&'static str, &'static str)>>,
54    },
55    /// Verifies JWT access tokens using the authorization server’s JSON Web Key
56    /// Set (JWKS) endpoint.
57    ///
58    /// This strategy allows fully offline signature validation after retrieving
59    /// the key set, making it efficient for high-throughput services.
60    JWKs {
61        /// The JWKS endpoint URL used to retrieve signing keys.
62        jwks_uri: String,
63    },
64    /// Verifies tokens by querying the OpenID Connect UserInfo endpoint.
65    ///
66    /// This strategy is typically used when token validity is tied to the user's
67    /// profile information or when the resource server relies on OIDC user data
68    /// for validation.
69    UserInfo { userinfo_uri: String },
70}
71
72/// Options for configuring a token verifier.
73///
74/// `TokenVerifierOptions` allows specifying one or more strategies for verifying
75/// OAuth access tokens. Multiple strategies can be provided; the verifier will
76/// attempt them in order until one succeeds or all fail.
77pub struct TokenVerifierOptions {
78    /// The list of token verification strategies to use.
79    /// Each strategy defines a different method for validating tokens, such as
80    /// introspection, JWKS signature validation, or querying the UserInfo endpoint.
81    /// For optimal performance, it is recommended to include JWKS alongside either introspection or UserInfo.
82    pub strategies: Vec<VerificationStrategies>,
83    /// Optional audience value to validate against the token's `aud` claim.
84    pub validate_audience: Option<Audience>,
85    /// Optional issuer value to validate against the token's `iss` claim.
86    pub validate_issuer: Option<String>,
87    /// Optional capacity for the internal cache, used to reduce unnecessary requests during verification.
88    pub cache_capacity: Option<usize>,
89}
90
91#[derive(Default, Debug)]
92struct StrategiesOptions {
93    pub introspection_uri: Option<Url>,
94    pub introspection_basic_auth: bool,
95    pub introspect_extra_params: Option<Vec<(&'static str, &'static str)>>,
96    pub client_id: Option<String>,
97    pub client_secret: Option<String>,
98    pub jwks_uri: Option<Url>,
99    pub userinfo_uri: Option<Url>,
100}
101
102impl TokenVerifierOptions {
103    fn unpack(&mut self) -> Result<(StrategiesOptions, bool), AuthenticationError> {
104        let mut result = StrategiesOptions::default();
105
106        let mut has_jwks = false;
107        let mut has_other = false;
108
109        for strategy in self.strategies.drain(0..) {
110            match strategy {
111                VerificationStrategies::Introspection {
112                    introspection_uri,
113                    client_id,
114                    client_secret,
115                    use_basic_auth,
116                    extra_params,
117                } => {
118                    result.introspection_uri =
119                        Some(Url::parse(&introspection_uri).map_err(|err| {
120                            AuthenticationError::ParsingError(format!(
121                                "Invalid introspection uri: {err}",
122                            ))
123                        })?);
124                    result.client_id = Some(client_id);
125                    result.client_secret = Some(client_secret);
126                    result.introspection_basic_auth = use_basic_auth;
127                    result.introspect_extra_params = extra_params;
128                    has_other = true;
129                }
130                VerificationStrategies::JWKs { jwks_uri } => {
131                    result.jwks_uri = Some(Url::parse(&jwks_uri).map_err(|err| {
132                        AuthenticationError::ParsingError(format!("Invalid jwks uri: {err}"))
133                    })?);
134                    has_jwks = true;
135                }
136                VerificationStrategies::UserInfo { userinfo_uri } => {
137                    result.userinfo_uri = Some(Url::parse(&userinfo_uri).map_err(|err| {
138                        AuthenticationError::ParsingError(format!("Invalid userinfo uri: {err}"))
139                    })?);
140                    has_other = true;
141                }
142            }
143        }
144
145        Ok((result, has_jwks && has_other))
146    }
147}
148
149pub struct GenericOauthTokenVerifier {
150    /// Optional audience value to validate against the token's `aud` claim.
151    validate_audience: Option<Audience>,
152    /// Optional issuer value to validate against the token's `iss` claim.
153    validate_issuer: Option<String>,
154    jwt_cache: Option<RwLock<JwtCache>>,
155    json_web_key_set: RwLock<Option<JwksCache>>,
156    introspection_uri: Option<Url>,
157    introspection_basic_auth: bool,
158    introspect_extra_params: Option<Vec<(&'static str, &'static str)>>,
159    client_id: Option<String>,
160    client_secret: Option<String>,
161    jwks_uri: Option<Url>,
162    userinfo_uri: Option<Url>,
163}
164
165impl GenericOauthTokenVerifier {
166    pub fn new(mut options: TokenVerifierOptions) -> Result<Self, AuthenticationError> {
167        let (strategy_options, chachable) = options.unpack()?;
168
169        let validate_audience = options.validate_audience.take();
170
171        let validate_issuer = options
172            .validate_issuer
173            .map(|iss| iss.trim_end_matches('/').to_string());
174
175        // we only need to cache if both jwks and introspection are supported
176        let jwt_cache = if chachable {
177            Some(RwLock::new(JwtCache::new(
178                REMOTE_VERIFICATION_INTERVAL,
179                options.cache_capacity.unwrap_or(JWT_CACHE_CAPACITY),
180            )))
181        } else {
182            None
183        };
184
185        Ok(Self {
186            validate_issuer,
187            validate_audience,
188            jwt_cache,
189            json_web_key_set: RwLock::new(None),
190            introspection_uri: strategy_options.introspection_uri,
191            introspection_basic_auth: strategy_options.introspection_basic_auth,
192            introspect_extra_params: strategy_options.introspect_extra_params,
193            client_id: strategy_options.client_id,
194            client_secret: strategy_options.client_secret,
195            jwks_uri: strategy_options.jwks_uri,
196            userinfo_uri: strategy_options.userinfo_uri,
197        })
198    }
199
200    async fn verify_user_info(
201        &self,
202        token: &str,
203        token_unique_id: Option<&str>,
204        user_info_endpoint: &Url,
205    ) -> Result<AuthInfo, AuthenticationError> {
206        // use token_unique_id or get from token header
207        let token_unique_id = match token_unique_id {
208            Some(id) => id.to_owned(),
209            None => {
210                let header = decode_token_header(token)?;
211                header.kid.unwrap_or(token.to_string()).to_owned()
212            }
213        };
214
215        let client = reqwest::Client::new();
216
217        let response = client
218            .get(user_info_endpoint.to_owned())
219            .header(AUTHORIZATION, format!("Bearer {token}"))
220            .send()
221            .await
222            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
223
224        let status_code = response.status();
225
226        if !response.status().is_success() {
227            return Err(AuthenticationError::TokenVerificationFailed {
228                description: error_message_from_response(response, "Unauthorized!").await,
229                status_code: Some(status_code.as_u16()),
230            });
231        }
232
233        let json: Value = response.json().await.unwrap();
234
235        let extra = match json {
236            Value::Object(map) => Some(map),
237            _ => None,
238        };
239
240        let auth_info: AuthInfo = AuthInfo {
241            token_unique_id,
242            client_id: None,
243            user_id: None,
244            scopes: None,
245            expires_at: None,
246            audience: None,
247            extra,
248        };
249
250        Ok(auth_info)
251    }
252
253    async fn verify_introspection(
254        &self,
255        token: &str,
256        introspection_endpoint: &Url,
257    ) -> Result<AuthInfo, AuthenticationError> {
258        let client = reqwest::Client::new();
259
260        // Form data body
261        let mut form = HashMap::new();
262        form.insert("token", token);
263
264        if !self.introspection_basic_auth {
265            if let Some(client_id) = self.client_id.as_ref() {
266                form.insert("client_id", client_id);
267            };
268            if let Some(client_secret) = self.client_secret.as_ref() {
269                form.insert("client_secret", client_secret);
270            };
271        }
272
273        if let Some(extra_params) = self.introspect_extra_params.as_ref() {
274            extra_params.iter().for_each(|(key, value)| {
275                form.insert(key, value);
276            });
277        }
278
279        let mut request = client.post(introspection_endpoint.to_owned()).form(&form);
280        if self.introspection_basic_auth {
281            request = request.basic_auth(
282                self.client_id.clone().unwrap_or_default(),
283                self.client_secret.clone(),
284            );
285        }
286
287        let response = request
288            .send()
289            .await
290            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
291
292        let status_code = response.status();
293        if !response.status().is_success() {
294            let description = response.text().await.unwrap_or("Unauthorized!".to_string());
295            return Err(AuthenticationError::TokenVerificationFailed {
296                description,
297                status_code: Some(status_code.as_u16()),
298            });
299        }
300
301        let introspect_response: IntrospectionResponse = response
302            .json()
303            .await
304            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
305
306        if !introspect_response.active {
307            return Err(AuthenticationError::InactiveToken);
308        }
309
310        if let Some(validate_audience) = self.validate_audience.as_ref() {
311            let Some(token_audience) = introspect_response.audience.as_ref() else {
312                return Err(AuthenticationError::InvalidToken {
313                    description: "Audience attribute (aud) is missing.",
314                });
315            };
316
317            if token_audience != validate_audience {
318                return Err(AuthenticationError::TokenVerificationFailed { description:
319                    format!("None of the provided audiences are allowed. Expected ${validate_audience}, got: ${token_audience}")
320                    , status_code: Some(StatusCode::UNAUTHORIZED.as_u16())
321                });
322            }
323        }
324
325        if let Some(validate_issuer) = self.validate_issuer.as_ref() {
326            let Some(token_issuer) = introspect_response.issuer.as_ref() else {
327                return Err(AuthenticationError::InvalidToken {
328                    description: "Issuer (iss) is missing.",
329                });
330            };
331
332            if token_issuer != validate_issuer {
333                return Err(AuthenticationError::TokenVerificationFailed {
334                    description: format!(
335                        "Issuer is not allowed. Expected ${validate_issuer}, got: ${token_issuer}"
336                    ),
337                    status_code: Some(StatusCode::UNAUTHORIZED.as_u16()),
338                });
339            }
340        }
341
342        AuthInfo::from_introspection_response(token.to_owned(), introspect_response, None)
343    }
344
345    async fn populate_jwks(&self, jwks_uri: &Url) -> Result<(), AuthenticationError> {
346        let response = reqwest::get(jwks_uri.to_owned())
347            .await
348            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
349        let jwks: JsonWebKeySet = response
350            .json()
351            .await
352            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
353        let mut guard = self.json_web_key_set.write().await;
354        *guard = Some(JwksCache {
355            last_updated: Some(SystemTime::now()),
356            jwks,
357        });
358        Ok(())
359    }
360
361    async fn verify_jwks(&self, token: &str, jwks: &Url) -> Result<AuthInfo, AuthenticationError> {
362        // read-modify-write pattern
363        {
364            let guard = self.json_web_key_set.read().await;
365            if let Some(cache) = guard.as_ref() {
366                if let Some(last_updated) = cache.last_updated {
367                    if SystemTime::now()
368                        .duration_since(last_updated)
369                        .unwrap_or(Duration::from_secs(0))
370                        < JWKS_REFRESH_TIME
371                    {
372                        let token_info = cache.jwks.verify(
373                            token.to_string(),
374                            self.validate_audience.as_ref(),
375                            self.validate_issuer.as_ref(),
376                        )?;
377
378                        return AuthInfo::from_token_data(token.to_owned(), token_info, None);
379                    }
380                }
381            }
382        }
383
384        // Refresh JWKS if cache is invalid or missing
385        self.populate_jwks(jwks).await?;
386
387        // Proceed with verification
388        let guard = self.json_web_key_set.read().await;
389        if let Some(cache) = guard.as_ref() {
390            let token_info = cache.jwks.verify(
391                token.to_string(),
392                self.validate_audience.as_ref(),
393                self.validate_issuer.as_ref(),
394            )?;
395
396            AuthInfo::from_token_data(token.to_owned(), token_info, None)
397        } else {
398            Err(AuthenticationError::Jwks(
399                "Failed to retrieve or parse JWKS".to_string(),
400            ))
401        }
402    }
403}
404
405#[async_trait]
406impl OauthTokenVerifier for GenericOauthTokenVerifier {
407    async fn verify_token(&self, access_token: String) -> Result<AuthInfo, AuthenticationError> {
408        // perform local jwks verification if supported
409        if let Some(jwks_endpoint) = self.jwks_uri.as_ref() {
410            let mut auth_info = self.verify_jwks(&access_token, jwks_endpoint).await?;
411
412            // perform remote verification only if it is supported and jwt is stale
413            if let Some(jwt_cache) = self.jwt_cache.as_ref() {
414                // return auth_info if it is recent
415                if jwt_cache.read().await.is_recent(&auth_info.token_unique_id) {
416                    return Ok(auth_info);
417                }
418
419                // introspection validation if introspection_uri is provided
420                if let Some(introspection_endpoint) = self.introspection_uri.as_ref() {
421                    let fresh_auth_info = self
422                        .verify_introspection(&access_token, introspection_endpoint)
423                        .await?;
424                    jwt_cache
425                        .write()
426                        .await
427                        .record(fresh_auth_info.token_unique_id.to_owned());
428                    return Ok(fresh_auth_info);
429                }
430
431                // call userInfo endpoint only if introspect strategy is not used
432                if let Some(user_info_endpoint) = self.userinfo_uri.as_ref() {
433                    let fresh_auth_info = self
434                        .verify_user_info(
435                            &access_token,
436                            Some(&auth_info.token_unique_id),
437                            user_info_endpoint,
438                        )
439                        .await?;
440
441                    auth_info.extra = fresh_auth_info.extra;
442                    jwt_cache
443                        .write()
444                        .await
445                        .record(auth_info.token_unique_id.to_owned());
446                    return Ok(auth_info);
447                }
448            }
449
450            return Ok(auth_info);
451        }
452
453        // use introspection if jwks is not supported, no caching
454        if let Some(introspection_endpoint) = self.introspection_uri.as_ref() {
455            let auth_info = self
456                .verify_introspection(&access_token, introspection_endpoint)
457                .await?;
458            return Ok(auth_info);
459        }
460
461        // use userInfo endpoint if introspect strategy is not used
462        if let Some(user_info_endpoint) = self.userinfo_uri.as_ref() {
463            let auth_info = self
464                .verify_user_info(&access_token, None, user_info_endpoint)
465                .await?;
466            return Ok(auth_info);
467        }
468
469        Err(AuthenticationError::InvalidToken {
470            description: "Invalid token verification strategy!",
471        })
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use oauth2_test_server::{OAuthTestServer, OauthEndpoints};
479    use rust_mcp_sdk::auth::*;
480    use serde_json::json;
481
482    async fn token_verifier(
483        strategies: Vec<VerificationStrategies>,
484        endpoints: &OauthEndpoints,
485        audience: Option<Audience>,
486    ) -> GenericOauthTokenVerifier {
487        let auth_metadata = AuthMetadataBuilder::new("http://127.0.0.1:3000/mcp")
488            .issuer(&endpoints.oauth_server)
489            .authorization_servers(vec![&endpoints.oauth_server])
490            .authorization_endpoint(&endpoints.authorize)
491            .token_endpoint(&endpoints.token)
492            .scopes_supported(vec!["openid".to_string()])
493            .introspection_endpoint(&endpoints.introspect)
494            .jwks_uri(&endpoints.jwks)
495            .resource_name("MCP Demo Server".to_string())
496            .build()
497            .unwrap();
498        let meta = &auth_metadata.0;
499
500        let token_verifier = GenericOauthTokenVerifier::new(TokenVerifierOptions {
501            validate_audience: audience,
502            validate_issuer: Some(meta.issuer.to_string()),
503            strategies,
504            cache_capacity: None,
505        })
506        .unwrap();
507        token_verifier
508    }
509
510    #[tokio::test]
511    async fn test_jwks_strategy() {
512        let server = OAuthTestServer::start().await;
513
514        let client = server.register_client(
515            json!({ "scope": "openid", "redirect_uris":["http://localhost:8080/callback"]}),
516        );
517
518        let verifier = token_verifier(
519            vec![VerificationStrategies::JWKs {
520                jwks_uri: server.endpoints.jwks.clone(),
521            }],
522            &server.endpoints,
523            Some(Audience::Single(client.client_id.clone())),
524        )
525        .await;
526
527        let token = server.generate_jwt(&client, server.jwt_options().user_id("rustmcp").build());
528
529        let auth_info = verifier.verify_token(token).await.unwrap();
530        assert_eq!(
531            auth_info.audience.as_ref().unwrap().to_string(),
532            client.client_id
533        );
534        assert_eq!(
535            auth_info.client_id.as_ref().unwrap().to_string(),
536            client.client_id
537        );
538        assert_eq!(auth_info.user_id.as_ref().unwrap(), "rustmcp");
539        let scopes = auth_info.scopes.as_ref().unwrap();
540        assert_eq!(scopes.as_slice(), ["openid"]);
541    }
542
543    #[tokio::test]
544    async fn test_userinfo_strategy() {
545        let server = OAuthTestServer::start().await;
546
547        let client = server.register_client(
548            json!({ "scope": "openid", "redirect_uris":["http://localhost:8080/callback"]}),
549        );
550
551        let verifier = token_verifier(
552            vec![VerificationStrategies::UserInfo {
553                userinfo_uri: server.endpoints.userinfo.clone(),
554            }],
555            &server.endpoints,
556            None,
557        )
558        .await;
559
560        let token = server.generate_token(&client, server.jwt_options().user_id("rustmcp").build());
561
562        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
563
564        assert!(auth_info.audience.is_none());
565        assert_eq!(
566            auth_info
567                .extra
568                .unwrap()
569                .get("sub")
570                .unwrap()
571                .as_str()
572                .unwrap(),
573            "rustmcp"
574        );
575    }
576
577    #[tokio::test]
578    async fn test_introspect_strategy() {
579        let server = OAuthTestServer::start().await;
580
581        let client = server.register_client(
582            json!({ "scope": "openid", "redirect_uris":["http://localhost:8080/callback"]}),
583        );
584
585        let verifier = token_verifier(
586            vec![VerificationStrategies::Introspection {
587                introspection_uri: server.endpoints.introspect.clone(),
588                client_id: client.client_id.clone(),
589                client_secret: client.client_secret.as_ref().unwrap().clone(),
590                use_basic_auth: true,
591                extra_params: None,
592            }],
593            &server.endpoints,
594            None,
595        )
596        .await;
597
598        let token = server.generate_token(&client, server.jwt_options().user_id("rustmcp").build());
599        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
600
601        assert_eq!(
602            auth_info.audience.as_ref().unwrap().to_string(),
603            client.client_id
604        );
605        assert_eq!(
606            auth_info.client_id.as_ref().unwrap().to_string(),
607            client.client_id
608        );
609        assert_eq!(auth_info.user_id.as_ref().unwrap(), "rustmcp");
610        let scopes = auth_info.scopes.as_ref().unwrap();
611        assert_eq!(scopes.as_slice(), ["openid"]);
612    }
613
614    #[tokio::test]
615    async fn test_introspect_strategy_with_client_secret_post() {
616        let server = OAuthTestServer::start().await;
617
618        let client = server.register_client(
619            json!({ "scope": "openid profile", "redirect_uris":["http://localhost:8080/cb"]}),
620        );
621
622        let verifier = token_verifier(
623            vec![VerificationStrategies::Introspection {
624                introspection_uri: server.endpoints.introspect.clone(),
625                client_id: client.client_id.clone(),
626                client_secret: client.client_secret.as_ref().unwrap().clone(),
627                use_basic_auth: false, // <--- POST body instead of Basic Auth
628                extra_params: None,
629            }],
630            &server.endpoints,
631            Some(Audience::Single(client.client_id.clone())),
632        )
633        .await;
634
635        let token = server.generate_token(&client, server.jwt_options().user_id("alice").build());
636
637        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
638
639        assert_eq!(auth_info.user_id.as_ref().unwrap(), "alice");
640        assert!(auth_info.scopes.unwrap().contains(&"profile".to_string()));
641        assert_eq!(
642            auth_info.audience.as_ref().unwrap().to_string(),
643            client.client_id
644        );
645    }
646
647    #[tokio::test]
648    async fn test_introspect_rejects_inactive_token() {
649        let server = OAuthTestServer::start().await;
650        let client = server
651            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }));
652
653        let verifier = token_verifier(
654            vec![VerificationStrategies::Introspection {
655                introspection_uri: server.endpoints.introspect.clone(),
656                client_id: client.client_id.clone(),
657                client_secret: client.client_secret.as_ref().unwrap().clone(),
658                use_basic_auth: true,
659                extra_params: None,
660            }],
661            &server.endpoints,
662            None,
663        )
664        .await;
665
666        let token_response =
667            server.generate_token(&client, server.jwt_options().user_id("bob").build());
668        server
669            .revoke_token(&client, &token_response.access_token)
670            .await;
671
672        let result = verifier.verify_token(token_response.access_token).await;
673        assert!(matches!(result, Err(AuthenticationError::InactiveToken)));
674    }
675
676    #[tokio::test]
677    async fn test_expired_token_rejected_by_jwks_and_introspection() {
678        let server = OAuthTestServer::start().await;
679        let client = server.register_client(
680            json!({ "scope": "openid email", "redirect_uris": ["http://localhost"] }),
681        );
682
683        // Use both strategies → expect rejection on expiration alone
684        let verifier = token_verifier(
685            vec![
686                VerificationStrategies::JWKs {
687                    jwks_uri: server.endpoints.jwks.clone(),
688                },
689                VerificationStrategies::Introspection {
690                    introspection_uri: server.endpoints.introspect.clone(),
691                    client_id: client.client_id.clone(),
692                    client_secret: client.client_secret.as_ref().unwrap().clone(),
693                    use_basic_auth: true,
694                    extra_params: None,
695                },
696            ],
697            &server.endpoints,
698            Some(Audience::Single(client.client_id.clone())),
699        )
700        .await;
701
702        // Generate short-lived token
703        let short_lived = server
704            .jwt_options()
705            .user_id("charlie")
706            .expires_in(1)
707            .build();
708        let token = server.generate_token(&client, short_lived);
709
710        // Wait for expiry
711        tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
712
713        // JWKS should reject immediately (exp validation)
714        // But since fallback is enabled, it hits introspection → active: false → error
715        let err1 = verifier
716            .verify_token(token.access_token.clone())
717            .await
718            .unwrap_err();
719        assert!(matches!(err1, AuthenticationError::InactiveToken));
720
721        // Now revoke it (expired + revoked) → still InactiveToken (no special handling needed)
722        server.revoke_token(&client, &token.access_token).await;
723        let err2 = verifier.verify_token(token.access_token).await.unwrap_err();
724        assert!(matches!(err2, AuthenticationError::InactiveToken));
725    }
726
727    #[tokio::test]
728    async fn test_jwks_and_introspection_cache_works() {
729        let server = OAuthTestServer::start().await;
730        let client = server
731            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }));
732
733        let verifier = token_verifier(
734            vec![
735                VerificationStrategies::JWKs {
736                    jwks_uri: server.endpoints.jwks.clone(),
737                },
738                VerificationStrategies::Introspection {
739                    introspection_uri: server.endpoints.introspect.clone(),
740                    client_id: client.client_id.clone(),
741                    client_secret: client.client_secret.as_ref().unwrap().clone(),
742                    use_basic_auth: true,
743                    extra_params: None,
744                },
745            ],
746            &server.endpoints,
747            None,
748        )
749        .await;
750
751        let token = server.generate_token(&client, server.jwt_options().user_id("dave").build());
752
753        // First call → goes through full flow
754        let info1 = verifier
755            .verify_token(token.access_token.clone())
756            .await
757            .unwrap();
758
759        // Second call → should hit cache (no network)
760        let info2 = verifier
761            .verify_token(token.access_token.clone())
762            .await
763            .unwrap();
764
765        assert_eq!(info1.user_id, info2.user_id);
766        assert_eq!(info1.token_unique_id, info2.token_unique_id);
767    }
768
769    #[tokio::test]
770    async fn test_audience_validation_rejects_wrong_aud() {
771        let server = OAuthTestServer::start().await;
772        let client = server
773            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }));
774
775        let verifier = token_verifier(
776            vec![VerificationStrategies::Introspection {
777                introspection_uri: server.endpoints.introspect.clone(),
778                client_id: client.client_id.clone(),
779                client_secret: client.client_secret.as_ref().unwrap().clone(),
780                use_basic_auth: true,
781                extra_params: None,
782            }],
783            &server.endpoints,
784            Some(Audience::Single("wrong-client-id-999".to_string())),
785        )
786        .await;
787
788        let token = server.generate_token(&client, server.jwt_options().user_id("eve").build());
789
790        let err = verifier.verify_token(token.access_token).await.unwrap_err();
791        assert!(matches!(
792            err,
793            AuthenticationError::TokenVerificationFailed { .. }
794        ));
795    }
796
797    #[tokio::test]
798    async fn test_issuer_validation_rejects_wrong_iss() {
799        let server = OAuthTestServer::start().await;
800        let client = server
801            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }));
802
803        let _verifier = token_verifier(
804            vec![VerificationStrategies::JWKs {
805                jwks_uri: server.endpoints.jwks.clone(),
806            }],
807            &server.endpoints,
808            None,
809        )
810        .await;
811
812        // Force wrong expected issuer
813        let wrong_verifier = GenericOauthTokenVerifier::new(TokenVerifierOptions {
814            strategies: vec![VerificationStrategies::JWKs {
815                jwks_uri: server.endpoints.jwks.clone(),
816            }],
817            validate_audience: None,
818            validate_issuer: Some("https://wrong-issuer.example.com".to_string()),
819            cache_capacity: None,
820        })
821        .unwrap();
822
823        let token = server.generate_token(&client, server.jwt_options().user_id("frank").build());
824
825        let err = wrong_verifier
826            .verify_token(token.access_token)
827            .await
828            .unwrap_err();
829        assert!(matches!(
830            err,
831            AuthenticationError::TokenVerificationFailed { .. }
832        ));
833    }
834
835    #[tokio::test]
836    async fn test_userinfo_enriches_jwt_claims() {
837        let server = OAuthTestServer::start().await;
838        let client = server.register_client(
839            json!({ "scope": "openid profile email", "redirect_uris": ["http://localhost"] }),
840        );
841
842        let verifier = token_verifier(
843            vec![
844                VerificationStrategies::JWKs {
845                    jwks_uri: server.endpoints.jwks.clone(),
846                },
847                VerificationStrategies::UserInfo {
848                    userinfo_uri: server.endpoints.userinfo.clone(),
849                },
850            ],
851            &server.endpoints,
852            None,
853        )
854        .await;
855
856        let token = server.generate_token(&client, server.jwt_options().user_id("grace").build());
857
858        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
859
860        let extra = auth_info.extra.unwrap();
861        assert_eq!(
862            extra.get("email").unwrap().as_str().unwrap(),
863            "test@example.com"
864        );
865        assert_eq!(extra.get("name").unwrap().as_str().unwrap(), "Test User");
866        assert!(extra.get("picture").is_some());
867    }
868}