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, default_jwks_algorithms, shared_http_client, Algorithm, Audience,
8        AuthInfo, AuthenticationError, IntrospectionResponse, 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    /// Signature algorithms accepted during JWKS verification.
155    allowed_algorithms: Vec<Algorithm>,
156    jwt_cache: Option<RwLock<JwtCache>>,
157    json_web_key_set: RwLock<Option<JwksCache>>,
158    introspection_uri: Option<Url>,
159    introspection_basic_auth: bool,
160    introspect_extra_params: Option<Vec<(&'static str, &'static str)>>,
161    client_id: Option<String>,
162    client_secret: Option<String>,
163    jwks_uri: Option<Url>,
164    userinfo_uri: Option<Url>,
165}
166
167impl GenericOauthTokenVerifier {
168    pub fn new(mut options: TokenVerifierOptions) -> Result<Self, AuthenticationError> {
169        let (strategy_options, chachable) = options.unpack()?;
170
171        let validate_audience = options.validate_audience.take();
172
173        let validate_issuer = options
174            .validate_issuer
175            .map(|iss| iss.trim_end_matches('/').to_string());
176
177        // we only need to cache if both jwks and introspection are supported
178        let jwt_cache = if chachable {
179            Some(RwLock::new(JwtCache::new(
180                REMOTE_VERIFICATION_INTERVAL,
181                options.cache_capacity.unwrap_or(JWT_CACHE_CAPACITY),
182            )))
183        } else {
184            None
185        };
186
187        Ok(Self {
188            validate_issuer,
189            validate_audience,
190            allowed_algorithms: default_jwks_algorithms(),
191            jwt_cache,
192            json_web_key_set: RwLock::new(None),
193            introspection_uri: strategy_options.introspection_uri,
194            introspection_basic_auth: strategy_options.introspection_basic_auth,
195            introspect_extra_params: strategy_options.introspect_extra_params,
196            client_id: strategy_options.client_id,
197            client_secret: strategy_options.client_secret,
198            jwks_uri: strategy_options.jwks_uri,
199            userinfo_uri: strategy_options.userinfo_uri,
200        })
201    }
202
203    /// Override the set of algorithms allowed during JWKS verification.
204    ///
205    /// By default only asymmetric algorithms (RS/PS/ES/EdDSA) are accepted.
206    /// Use this builder method if you need a custom allowlist.
207    pub fn with_allowed_algorithms(mut self, algorithms: Vec<Algorithm>) -> Self {
208        self.allowed_algorithms = algorithms;
209        self
210    }
211
212    async fn verify_user_info(
213        &self,
214        token: &str,
215        token_unique_id: Option<&str>,
216        user_info_endpoint: &Url,
217    ) -> Result<AuthInfo, AuthenticationError> {
218        // use token_unique_id or get from token header
219        let token_unique_id = match token_unique_id {
220            Some(id) => id.to_owned(),
221            None => {
222                let header = decode_token_header(token)?;
223                header.kid.unwrap_or(token.to_string()).to_owned()
224            }
225        };
226
227        let client = shared_http_client();
228
229        let response = client
230            .get(user_info_endpoint.to_owned())
231            .header(AUTHORIZATION, format!("Bearer {token}"))
232            .send()
233            .await
234            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
235
236        let status_code = response.status();
237
238        if !response.status().is_success() {
239            return Err(AuthenticationError::TokenVerificationFailed {
240                description: error_message_from_response(response, "Unauthorized!").await,
241                status_code: Some(status_code.as_u16()),
242            });
243        }
244
245        let json: Value = response.json().await.unwrap();
246
247        let extra = match json {
248            Value::Object(map) => Some(map),
249            _ => None,
250        };
251
252        let auth_info: AuthInfo = AuthInfo {
253            token_unique_id,
254            client_id: None,
255            user_id: None,
256            scopes: None,
257            expires_at: None,
258            audience: None,
259            extra,
260        };
261
262        Ok(auth_info)
263    }
264
265    async fn verify_introspection(
266        &self,
267        token: &str,
268        introspection_endpoint: &Url,
269    ) -> Result<AuthInfo, AuthenticationError> {
270        let client = shared_http_client();
271
272        // Form data body
273        let mut form = HashMap::new();
274        form.insert("token", token);
275
276        if !self.introspection_basic_auth {
277            if let Some(client_id) = self.client_id.as_ref() {
278                form.insert("client_id", client_id);
279            };
280            if let Some(client_secret) = self.client_secret.as_ref() {
281                form.insert("client_secret", client_secret);
282            };
283        }
284
285        if let Some(extra_params) = self.introspect_extra_params.as_ref() {
286            extra_params.iter().for_each(|(key, value)| {
287                form.insert(key, value);
288            });
289        }
290
291        let mut request = client.post(introspection_endpoint.to_owned()).form(&form);
292        if self.introspection_basic_auth {
293            request = request.basic_auth(
294                self.client_id.clone().unwrap_or_default(),
295                self.client_secret.clone(),
296            );
297        }
298
299        let response = request
300            .send()
301            .await
302            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
303
304        let status_code = response.status();
305        if !response.status().is_success() {
306            let description = response.text().await.unwrap_or("Unauthorized!".to_string());
307            return Err(AuthenticationError::TokenVerificationFailed {
308                description,
309                status_code: Some(status_code.as_u16()),
310            });
311        }
312
313        let introspect_response: IntrospectionResponse = response
314            .json()
315            .await
316            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
317
318        if !introspect_response.active {
319            return Err(AuthenticationError::InactiveToken);
320        }
321
322        if let Some(validate_audience) = self.validate_audience.as_ref() {
323            let Some(token_audience) = introspect_response.audience.as_ref() else {
324                return Err(AuthenticationError::InvalidToken {
325                    description: "Audience attribute (aud) is missing.",
326                });
327            };
328
329            if token_audience != validate_audience {
330                return Err(AuthenticationError::TokenVerificationFailed { description:
331                    format!("None of the provided audiences are allowed. Expected ${validate_audience}, got: ${token_audience}")
332                    , status_code: Some(StatusCode::UNAUTHORIZED.as_u16())
333                });
334            }
335        }
336
337        if let Some(validate_issuer) = self.validate_issuer.as_ref() {
338            let Some(token_issuer) = introspect_response.issuer.as_ref() else {
339                return Err(AuthenticationError::InvalidToken {
340                    description: "Issuer (iss) is missing.",
341                });
342            };
343
344            if token_issuer != validate_issuer {
345                return Err(AuthenticationError::TokenVerificationFailed {
346                    description: format!(
347                        "Issuer is not allowed. Expected ${validate_issuer}, got: ${token_issuer}"
348                    ),
349                    status_code: Some(StatusCode::UNAUTHORIZED.as_u16()),
350                });
351            }
352        }
353
354        AuthInfo::from_introspection_response(token.to_owned(), introspect_response, None)
355    }
356
357    async fn populate_jwks(&self, jwks_uri: &Url) -> Result<(), AuthenticationError> {
358        let response = shared_http_client()
359            .get(jwks_uri.to_owned())
360            .send()
361            .await
362            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
363        let jwks: JsonWebKeySet = response
364            .json()
365            .await
366            .map_err(|err| AuthenticationError::Jwks(err.to_string()))?;
367        let mut guard = self.json_web_key_set.write().await;
368        *guard = Some(JwksCache {
369            last_updated: Some(SystemTime::now()),
370            jwks,
371        });
372        Ok(())
373    }
374
375    async fn verify_jwks(&self, token: &str, jwks: &Url) -> Result<AuthInfo, AuthenticationError> {
376        // read-modify-write pattern
377        {
378            let guard = self.json_web_key_set.read().await;
379            if let Some(cache) = guard.as_ref() {
380                if let Some(last_updated) = cache.last_updated {
381                    if SystemTime::now()
382                        .duration_since(last_updated)
383                        .unwrap_or(Duration::from_secs(0))
384                        < JWKS_REFRESH_TIME
385                    {
386                        let token_info = cache.jwks.verify(
387                            token.to_string(),
388                            &self.allowed_algorithms,
389                            self.validate_audience.as_ref(),
390                            self.validate_issuer.as_ref(),
391                        )?;
392
393                        return AuthInfo::from_token_data(token.to_owned(), token_info, None);
394                    }
395                }
396            }
397        }
398
399        // Refresh JWKS if cache is invalid or missing
400        self.populate_jwks(jwks).await?;
401
402        // Proceed with verification
403        let guard = self.json_web_key_set.read().await;
404        if let Some(cache) = guard.as_ref() {
405            let token_info = cache.jwks.verify(
406                token.to_string(),
407                &self.allowed_algorithms,
408                self.validate_audience.as_ref(),
409                self.validate_issuer.as_ref(),
410            )?;
411
412            AuthInfo::from_token_data(token.to_owned(), token_info, None)
413        } else {
414            Err(AuthenticationError::Jwks(
415                "Failed to retrieve or parse JWKS".to_string(),
416            ))
417        }
418    }
419}
420
421#[async_trait]
422impl OauthTokenVerifier for GenericOauthTokenVerifier {
423    async fn verify_token(&self, access_token: String) -> Result<AuthInfo, AuthenticationError> {
424        // perform local jwks verification if supported
425        if let Some(jwks_endpoint) = self.jwks_uri.as_ref() {
426            let mut auth_info = self.verify_jwks(&access_token, jwks_endpoint).await?;
427
428            // perform remote verification only if it is supported and jwt is stale
429            if let Some(jwt_cache) = self.jwt_cache.as_ref() {
430                // return auth_info if it is recent
431                if jwt_cache.read().await.is_recent(&auth_info.token_unique_id) {
432                    return Ok(auth_info);
433                }
434
435                // introspection validation if introspection_uri is provided
436                if let Some(introspection_endpoint) = self.introspection_uri.as_ref() {
437                    let fresh_auth_info = self
438                        .verify_introspection(&access_token, introspection_endpoint)
439                        .await?;
440                    jwt_cache
441                        .write()
442                        .await
443                        .record(fresh_auth_info.token_unique_id.to_owned());
444                    return Ok(fresh_auth_info);
445                }
446
447                // call userInfo endpoint only if introspect strategy is not used
448                if let Some(user_info_endpoint) = self.userinfo_uri.as_ref() {
449                    let fresh_auth_info = self
450                        .verify_user_info(
451                            &access_token,
452                            Some(&auth_info.token_unique_id),
453                            user_info_endpoint,
454                        )
455                        .await?;
456
457                    auth_info.extra = fresh_auth_info.extra;
458                    jwt_cache
459                        .write()
460                        .await
461                        .record(auth_info.token_unique_id.to_owned());
462                    return Ok(auth_info);
463                }
464            }
465
466            return Ok(auth_info);
467        }
468
469        // use introspection if jwks is not supported, no caching
470        if let Some(introspection_endpoint) = self.introspection_uri.as_ref() {
471            let auth_info = self
472                .verify_introspection(&access_token, introspection_endpoint)
473                .await?;
474            return Ok(auth_info);
475        }
476
477        // use userInfo endpoint if introspect strategy is not used
478        if let Some(user_info_endpoint) = self.userinfo_uri.as_ref() {
479            let auth_info = self
480                .verify_user_info(&access_token, None, user_info_endpoint)
481                .await?;
482            return Ok(auth_info);
483        }
484
485        Err(AuthenticationError::InvalidToken {
486            description: "Invalid token verification strategy!",
487        })
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494    use oauth2_test_server::{OAuthTestServer, OauthEndpoints};
495    use rust_mcp_sdk::auth::*;
496    use serde_json::json;
497
498    async fn token_verifier(
499        strategies: Vec<VerificationStrategies>,
500        endpoints: &OauthEndpoints,
501        audience: Option<Audience>,
502    ) -> GenericOauthTokenVerifier {
503        let auth_metadata = AuthMetadataBuilder::new("http://127.0.0.1:3000/mcp")
504            .issuer(&endpoints.oauth_server)
505            .authorization_servers(vec![&endpoints.oauth_server])
506            .authorization_endpoint(&endpoints.authorize)
507            .token_endpoint(&endpoints.token)
508            .scopes_supported(vec!["openid".to_string()])
509            .introspection_endpoint(&endpoints.introspect)
510            .jwks_uri(&endpoints.jwks)
511            .resource_name("MCP Demo Server".to_string())
512            .build()
513            .unwrap();
514        let meta = &auth_metadata.0;
515
516        let token_verifier = GenericOauthTokenVerifier::new(TokenVerifierOptions {
517            validate_audience: audience,
518            validate_issuer: Some(meta.issuer.to_string()),
519            strategies,
520            cache_capacity: None,
521        })
522        .unwrap();
523        token_verifier
524    }
525
526    #[tokio::test]
527    async fn test_jwks_strategy() {
528        let server = OAuthTestServer::start().await;
529
530        let client = server
531            .register_client(
532                json!({ "scope": "openid", "redirect_uris":["http://localhost:8080/callback"]}),
533            )
534            .await;
535
536        let verifier = token_verifier(
537            vec![VerificationStrategies::JWKs {
538                jwks_uri: server.endpoints.jwks.clone(),
539            }],
540            &server.endpoints,
541            Some(Audience::Single(client.client_id.clone())),
542        )
543        .await;
544
545        let token = server.generate_jwt(&client, server.jwt_options().user_id("rustmcp").build());
546
547        let auth_info = verifier.verify_token(token).await.unwrap();
548        assert_eq!(
549            auth_info.audience.as_ref().unwrap().to_string(),
550            client.client_id
551        );
552        assert_eq!(
553            auth_info.client_id.as_ref().unwrap().to_string(),
554            client.client_id
555        );
556        assert_eq!(auth_info.user_id.as_ref().unwrap(), "rustmcp");
557        let scopes = auth_info.scopes.as_ref().unwrap();
558        assert_eq!(scopes.as_slice(), ["openid"]);
559    }
560
561    #[tokio::test]
562    async fn test_userinfo_strategy() {
563        let server = OAuthTestServer::start().await;
564
565        let client = server
566            .register_client(
567                json!({ "scope": "openid", "redirect_uris":["http://localhost:8080/callback"]}),
568            )
569            .await;
570
571        let verifier = token_verifier(
572            vec![VerificationStrategies::UserInfo {
573                userinfo_uri: server.endpoints.userinfo.clone(),
574            }],
575            &server.endpoints,
576            None,
577        )
578        .await;
579
580        let token = server
581            .generate_token(&client, server.jwt_options().user_id("rustmcp").build())
582            .await;
583
584        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
585
586        assert!(auth_info.audience.is_none());
587        assert_eq!(
588            auth_info
589                .extra
590                .unwrap()
591                .get("sub")
592                .unwrap()
593                .as_str()
594                .unwrap(),
595            "rustmcp"
596        );
597    }
598
599    #[tokio::test]
600    async fn test_introspect_strategy() {
601        let server = OAuthTestServer::start().await;
602
603        let client = server
604            .register_client(
605                json!({ "scope": "openid", "redirect_uris":["http://localhost:8080/callback"]}),
606            )
607            .await;
608
609        let verifier = token_verifier(
610            vec![VerificationStrategies::Introspection {
611                introspection_uri: server.endpoints.introspect.clone(),
612                client_id: client.client_id.clone(),
613                client_secret: client.client_secret.as_ref().unwrap().clone(),
614                use_basic_auth: true,
615                extra_params: None,
616            }],
617            &server.endpoints,
618            None,
619        )
620        .await;
621
622        let token = server
623            .generate_token(&client, server.jwt_options().user_id("rustmcp").build())
624            .await;
625        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
626
627        assert_eq!(
628            auth_info.audience.as_ref().unwrap().to_string(),
629            client.client_id
630        );
631        assert_eq!(
632            auth_info.client_id.as_ref().unwrap().to_string(),
633            client.client_id
634        );
635        assert_eq!(auth_info.user_id.as_ref().unwrap(), "rustmcp");
636        let scopes = auth_info.scopes.as_ref().unwrap();
637        assert_eq!(scopes.as_slice(), ["openid"]);
638    }
639
640    #[tokio::test]
641    async fn test_introspect_strategy_with_client_secret_post() {
642        let server = OAuthTestServer::start().await;
643
644        let client = server
645            .register_client(
646                json!({ "scope": "openid profile", "redirect_uris":["http://localhost:8080/cb"]}),
647            )
648            .await;
649
650        let verifier = token_verifier(
651            vec![VerificationStrategies::Introspection {
652                introspection_uri: server.endpoints.introspect.clone(),
653                client_id: client.client_id.clone(),
654                client_secret: client.client_secret.as_ref().unwrap().clone(),
655                use_basic_auth: false, // <--- POST body instead of Basic Auth
656                extra_params: None,
657            }],
658            &server.endpoints,
659            Some(Audience::Single(client.client_id.clone())),
660        )
661        .await;
662
663        let token = server
664            .generate_token(&client, server.jwt_options().user_id("alice").build())
665            .await;
666
667        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
668
669        assert_eq!(auth_info.user_id.as_ref().unwrap(), "alice");
670        assert!(auth_info.scopes.unwrap().contains(&"profile".to_string()));
671        assert_eq!(
672            auth_info.audience.as_ref().unwrap().to_string(),
673            client.client_id
674        );
675    }
676
677    #[tokio::test]
678    async fn test_introspect_rejects_inactive_token() {
679        let server = OAuthTestServer::start().await;
680        let client = server
681            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }))
682            .await;
683
684        let verifier = token_verifier(
685            vec![VerificationStrategies::Introspection {
686                introspection_uri: server.endpoints.introspect.clone(),
687                client_id: client.client_id.clone(),
688                client_secret: client.client_secret.as_ref().unwrap().clone(),
689                use_basic_auth: true,
690                extra_params: None,
691            }],
692            &server.endpoints,
693            None,
694        )
695        .await;
696
697        let token_response = server
698            .generate_token(&client, server.jwt_options().user_id("bob").build())
699            .await;
700        server
701            .revoke_token(&client, &token_response.access_token)
702            .await;
703
704        let result = verifier.verify_token(token_response.access_token).await;
705        assert!(matches!(result, Err(AuthenticationError::InactiveToken)));
706    }
707
708    #[tokio::test]
709    async fn test_expired_token_rejected_by_jwks_and_introspection() {
710        let server = OAuthTestServer::start().await;
711        let client = server
712            .register_client(
713                json!({ "scope": "openid email", "redirect_uris": ["http://localhost"] }),
714            )
715            .await;
716
717        // Use both strategies → expect rejection on expiration alone
718        let verifier = token_verifier(
719            vec![
720                VerificationStrategies::JWKs {
721                    jwks_uri: server.endpoints.jwks.clone(),
722                },
723                VerificationStrategies::Introspection {
724                    introspection_uri: server.endpoints.introspect.clone(),
725                    client_id: client.client_id.clone(),
726                    client_secret: client.client_secret.as_ref().unwrap().clone(),
727                    use_basic_auth: true,
728                    extra_params: None,
729                },
730            ],
731            &server.endpoints,
732            Some(Audience::Single(client.client_id.clone())),
733        )
734        .await;
735
736        // Generate short-lived token
737        let short_lived = server
738            .jwt_options()
739            .user_id("charlie")
740            .expires_in(1)
741            .build();
742        let token = server.generate_token(&client, short_lived).await;
743
744        // Wait for expiry
745        tokio::time::sleep(tokio::time::Duration::from_millis(2500)).await;
746
747        // JWKS should reject immediately (exp validation)
748        // But since fallback is enabled, it hits introspection → active: false → error
749        let err1 = verifier
750            .verify_token(token.access_token.clone())
751            .await
752            .unwrap_err();
753        assert!(matches!(err1, AuthenticationError::InactiveToken));
754
755        // Now revoke it (expired + revoked) → still InactiveToken (no special handling needed)
756        server.revoke_token(&client, &token.access_token).await;
757        let err2 = verifier.verify_token(token.access_token).await.unwrap_err();
758        assert!(matches!(err2, AuthenticationError::InactiveToken));
759    }
760
761    #[tokio::test]
762    async fn test_jwks_and_introspection_cache_works() {
763        let server = OAuthTestServer::start().await;
764        let client = server
765            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }))
766            .await;
767
768        let verifier = token_verifier(
769            vec![
770                VerificationStrategies::JWKs {
771                    jwks_uri: server.endpoints.jwks.clone(),
772                },
773                VerificationStrategies::Introspection {
774                    introspection_uri: server.endpoints.introspect.clone(),
775                    client_id: client.client_id.clone(),
776                    client_secret: client.client_secret.as_ref().unwrap().clone(),
777                    use_basic_auth: true,
778                    extra_params: None,
779                },
780            ],
781            &server.endpoints,
782            None,
783        )
784        .await;
785
786        let token = server
787            .generate_token(&client, server.jwt_options().user_id("dave").build())
788            .await;
789
790        // First call → goes through full flow
791        let info1 = verifier
792            .verify_token(token.access_token.clone())
793            .await
794            .unwrap();
795
796        // Second call → should hit cache (no network)
797        let info2 = verifier
798            .verify_token(token.access_token.clone())
799            .await
800            .unwrap();
801
802        assert_eq!(info1.user_id, info2.user_id);
803        assert_eq!(info1.token_unique_id, info2.token_unique_id);
804    }
805
806    #[tokio::test]
807    async fn test_audience_validation_rejects_wrong_aud() {
808        let server = OAuthTestServer::start().await;
809        let client = server
810            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }))
811            .await;
812
813        let verifier = token_verifier(
814            vec![VerificationStrategies::Introspection {
815                introspection_uri: server.endpoints.introspect.clone(),
816                client_id: client.client_id.clone(),
817                client_secret: client.client_secret.as_ref().unwrap().clone(),
818                use_basic_auth: true,
819                extra_params: None,
820            }],
821            &server.endpoints,
822            Some(Audience::Single("wrong-client-id-999".to_string())),
823        )
824        .await;
825
826        let token = server
827            .generate_token(&client, server.jwt_options().user_id("eve").build())
828            .await;
829
830        let err = verifier.verify_token(token.access_token).await.unwrap_err();
831        assert!(matches!(
832            err,
833            AuthenticationError::TokenVerificationFailed { .. }
834        ));
835    }
836
837    #[tokio::test]
838    async fn test_issuer_validation_rejects_wrong_iss() {
839        let server = OAuthTestServer::start().await;
840        let client = server
841            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }))
842            .await;
843
844        let _verifier = token_verifier(
845            vec![VerificationStrategies::JWKs {
846                jwks_uri: server.endpoints.jwks.clone(),
847            }],
848            &server.endpoints,
849            None,
850        )
851        .await;
852
853        // Force wrong expected issuer
854        let wrong_verifier = GenericOauthTokenVerifier::new(TokenVerifierOptions {
855            strategies: vec![VerificationStrategies::JWKs {
856                jwks_uri: server.endpoints.jwks.clone(),
857            }],
858            validate_audience: None,
859            validate_issuer: Some("https://wrong-issuer.example.com".to_string()),
860            cache_capacity: None,
861        })
862        .unwrap();
863
864        let token = server
865            .generate_token(&client, server.jwt_options().user_id("frank").build())
866            .await;
867
868        let err = wrong_verifier
869            .verify_token(token.access_token)
870            .await
871            .unwrap_err();
872        assert!(matches!(
873            err,
874            AuthenticationError::TokenVerificationFailed { .. }
875        ));
876    }
877
878    #[tokio::test]
879    async fn test_userinfo_enriches_jwt_claims() {
880        let server = OAuthTestServer::start().await;
881        let client = server
882            .register_client(
883                json!({ "scope": "openid profile email", "redirect_uris": ["http://localhost"] }),
884            )
885            .await;
886
887        let verifier = token_verifier(
888            vec![
889                VerificationStrategies::JWKs {
890                    jwks_uri: server.endpoints.jwks.clone(),
891                },
892                VerificationStrategies::UserInfo {
893                    userinfo_uri: server.endpoints.userinfo.clone(),
894                },
895            ],
896            &server.endpoints,
897            None,
898        )
899        .await;
900
901        let token = server
902            .generate_token(&client, server.jwt_options().user_id("grace").build())
903            .await;
904
905        let auth_info = verifier.verify_token(token.access_token).await.unwrap();
906
907        let extra = auth_info.extra.unwrap();
908        assert_eq!(
909            extra.get("email").unwrap().as_str().unwrap(),
910            "test@example.com"
911        );
912        assert_eq!(extra.get("name").unwrap().as_str().unwrap(), "Test User");
913        assert!(extra.get("picture").is_some());
914    }
915
916    #[tokio::test]
917    async fn test_with_allowed_algorithms_rejects_restricted_allowlist() {
918        let server = OAuthTestServer::start().await;
919        let client = server
920            .register_client(json!({ "scope": "openid", "redirect_uris": ["http://localhost"] }))
921            .await;
922
923        let verifier = token_verifier(
924            vec![VerificationStrategies::JWKs {
925                jwks_uri: server.endpoints.jwks.clone(),
926            }],
927            &server.endpoints,
928            None,
929        )
930        .await
931        .with_allowed_algorithms(vec![Algorithm::ES256]);
932
933        let token = server.generate_jwt(&client, server.jwt_options().user_id("hal").build());
934
935        let err = verifier.verify_token(token).await.unwrap_err();
936        assert!(matches!(
937            err,
938            AuthenticationError::TokenVerificationFailed { .. }
939        ));
940    }
941}