Skip to main content

fastmcp_server/
auth.rs

1//! Authentication provider hooks for MCP servers.
2//!
3//! Auth providers are transport-agnostic and operate on the JSON-RPC
4//! request payload. They may populate [`AuthContext`] to be stored in
5//! session state for downstream handlers.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use fastmcp_core::{AccessToken, AuthContext, McpContext, McpError, McpErrorCode, McpResult};
11
12/// Authentication request view used by providers.
13#[derive(Debug, Clone, Copy)]
14pub struct AuthRequest<'a> {
15    /// JSON-RPC method name.
16    pub method: &'a str,
17    /// Raw params payload (if present).
18    pub params: Option<&'a serde_json::Value>,
19    /// Internal request ID (u64) used for tracing.
20    pub request_id: u64,
21}
22
23impl AuthRequest<'_> {
24    /// Attempts to extract an access token from the raw request params.
25    #[must_use]
26    pub fn access_token(&self) -> Option<AccessToken> {
27        extract_access_token(self.params)
28    }
29}
30
31/// Extracts an access token from request params using common field names.
32fn extract_access_token(params: Option<&serde_json::Value>) -> Option<AccessToken> {
33    let params = params?;
34    match params {
35        serde_json::Value::String(value) => AccessToken::parse(value),
36        serde_json::Value::Object(map) => {
37            if let Some(token) = extract_from_map(map) {
38                return Some(token);
39            }
40            if let Some(meta) = map.get("_meta").and_then(serde_json::Value::as_object) {
41                if let Some(token) = extract_from_map(meta) {
42                    return Some(token);
43                }
44            }
45            if let Some(headers) = map.get("headers").and_then(serde_json::Value::as_object) {
46                if let Some(token) = extract_from_map(headers) {
47                    return Some(token);
48                }
49            }
50            None
51        }
52        _ => None,
53    }
54}
55
56fn extract_from_map(map: &serde_json::Map<String, serde_json::Value>) -> Option<AccessToken> {
57    for key in [
58        "authorization",
59        "Authorization",
60        "auth",
61        "token",
62        "access_token",
63        "accessToken",
64    ] {
65        if let Some(value) = map.get(key) {
66            if let Some(token) = extract_from_value(value) {
67                return Some(token);
68            }
69        }
70    }
71    None
72}
73
74fn extract_from_value(value: &serde_json::Value) -> Option<AccessToken> {
75    match value {
76        serde_json::Value::String(value) => AccessToken::parse(value),
77        serde_json::Value::Object(map) => {
78            if let (Some(scheme), Some(token)) = (
79                map.get("scheme").and_then(serde_json::Value::as_str),
80                map.get("token").and_then(serde_json::Value::as_str),
81            ) {
82                if !scheme.trim().is_empty() && !token.trim().is_empty() {
83                    return Some(AccessToken {
84                        scheme: scheme.trim().to_string(),
85                        token: token.trim().to_string(),
86                    });
87                }
88            }
89            for key in ["authorization", "token", "access_token", "accessToken"] {
90                if let Some(value) = map.get(key).and_then(serde_json::Value::as_str) {
91                    if let Some(token) = AccessToken::parse(value) {
92                        return Some(token);
93                    }
94                }
95            }
96            None
97        }
98        _ => None,
99    }
100}
101
102/// Authentication provider interface.
103///
104/// Implementations decide whether a request is allowed and may return
105/// an [`AuthContext`] describing the authenticated subject.
106pub trait AuthProvider: Send + Sync {
107    /// Authenticate an incoming request.
108    ///
109    /// Return `Ok(AuthContext)` to allow, or an `Err(McpError)` to deny.
110    fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext>;
111}
112
113/// Token verifier interface used by token-based auth providers.
114pub trait TokenVerifier: Send + Sync {
115    /// Verify an access token and return an auth context if valid.
116    fn verify(
117        &self,
118        ctx: &McpContext,
119        request: AuthRequest<'_>,
120        token: &AccessToken,
121    ) -> McpResult<AuthContext>;
122}
123
124/// Token-based authentication provider.
125#[derive(Clone)]
126pub struct TokenAuthProvider {
127    verifier: Arc<dyn TokenVerifier>,
128    missing_token_error: McpError,
129}
130
131impl TokenAuthProvider {
132    /// Creates a new token auth provider with the given verifier.
133    #[must_use]
134    pub fn new<V: TokenVerifier + 'static>(verifier: V) -> Self {
135        Self {
136            verifier: Arc::new(verifier),
137            missing_token_error: auth_error("Missing access token"),
138        }
139    }
140
141    /// Overrides the error returned when a token is missing.
142    #[must_use]
143    pub fn with_missing_token_error(mut self, error: McpError) -> Self {
144        self.missing_token_error = error;
145        self
146    }
147}
148
149impl AuthProvider for TokenAuthProvider {
150    fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext> {
151        let access = request
152            .access_token()
153            .ok_or_else(|| self.missing_token_error.clone())?;
154        self.verifier.verify(ctx, request, &access)
155    }
156}
157
158/// Static token verifier backed by an in-memory token map.
159#[derive(Debug, Clone)]
160pub struct StaticTokenVerifier {
161    tokens: HashMap<String, AuthContext>,
162    allowed_schemes: Option<Vec<String>>,
163}
164
165impl StaticTokenVerifier {
166    /// Creates a new static verifier from a token → context map.
167    pub fn new<I, K>(tokens: I) -> Self
168    where
169        I: IntoIterator<Item = (K, AuthContext)>,
170        K: Into<String>,
171    {
172        let tokens = tokens
173            .into_iter()
174            .map(|(token, ctx)| (token.into(), ctx))
175            .collect();
176        Self {
177            tokens,
178            allowed_schemes: None,
179        }
180    }
181
182    /// Restricts accepted token schemes (case-insensitive).
183    #[must_use]
184    pub fn with_allowed_schemes<I, S>(mut self, schemes: I) -> Self
185    where
186        I: IntoIterator<Item = S>,
187        S: Into<String>,
188    {
189        self.allowed_schemes = Some(schemes.into_iter().map(Into::into).collect());
190        self
191    }
192}
193
194impl TokenVerifier for StaticTokenVerifier {
195    fn verify(
196        &self,
197        _ctx: &McpContext,
198        _request: AuthRequest<'_>,
199        token: &AccessToken,
200    ) -> McpResult<AuthContext> {
201        if let Some(allowed) = &self.allowed_schemes {
202            if !allowed
203                .iter()
204                .any(|scheme| scheme.eq_ignore_ascii_case(&token.scheme))
205            {
206                return Err(auth_error("Unsupported auth scheme"));
207            }
208        }
209
210        let Some(auth) = self.tokens.get(&token.token) else {
211            return Err(auth_error("Invalid access token"));
212        };
213
214        let mut ctx = auth.clone();
215        ctx.token.get_or_insert_with(|| token.clone());
216        Ok(ctx)
217    }
218}
219
220fn auth_error(message: impl Into<String>) -> McpError {
221    McpError::new(McpErrorCode::ResourceForbidden, message)
222}
223
224#[cfg(feature = "jwt")]
225mod jwt {
226    use super::{AuthContext, AuthRequest, TokenVerifier, auth_error};
227    use fastmcp_core::{AccessToken, McpContext, McpResult};
228    use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
229
230    /// JWT verifier for HMAC-SHA tokens.
231    #[derive(Debug, Clone)]
232    pub struct JwtTokenVerifier {
233        decoding_key: DecodingKey,
234        validation: Validation,
235    }
236
237    impl JwtTokenVerifier {
238        /// Creates an HS256 verifier from a shared secret.
239        #[must_use]
240        pub fn hs256(secret: impl AsRef<[u8]>) -> Self {
241            Self {
242                decoding_key: DecodingKey::from_secret(secret.as_ref()),
243                validation: Validation::new(Algorithm::HS256),
244            }
245        }
246
247        /// Overrides the JWT validation settings.
248        #[must_use]
249        pub fn with_validation(mut self, validation: Validation) -> Self {
250            self.validation = validation;
251            self
252        }
253    }
254
255    impl TokenVerifier for JwtTokenVerifier {
256        fn verify(
257            &self,
258            _ctx: &McpContext,
259            _request: AuthRequest<'_>,
260            token: &AccessToken,
261        ) -> McpResult<AuthContext> {
262            if !token.scheme.eq_ignore_ascii_case("Bearer") {
263                return Err(auth_error("Unsupported auth scheme"));
264            }
265
266            let data =
267                decode::<serde_json::Value>(&token.token, &self.decoding_key, &self.validation)
268                    .map_err(|err| auth_error(format!("Invalid token: {err}")))?;
269
270            let claims = data.claims;
271            let subject = claims
272                .get("sub")
273                .and_then(serde_json::Value::as_str)
274                .map(str::to_string);
275            let scopes = extract_scopes(&claims);
276
277            Ok(AuthContext {
278                subject,
279                scopes,
280                token: Some(token.clone()),
281                claims: Some(claims),
282            })
283        }
284    }
285
286    fn extract_scopes(claims: &serde_json::Value) -> Vec<String> {
287        let mut scopes = Vec::new();
288        if let Some(scope) = claims.get("scope").and_then(serde_json::Value::as_str) {
289            scopes.extend(scope.split_whitespace().map(str::to_string));
290        }
291        if let Some(list) = claims.get("scopes").and_then(serde_json::Value::as_array) {
292            scopes.extend(
293                list.iter()
294                    .filter_map(|value| value.as_str().map(str::to_string)),
295            );
296        }
297        scopes
298    }
299}
300
301#[cfg(feature = "jwt")]
302pub use jwt::JwtTokenVerifier;
303
304/// Default allow-all provider (returns anonymous auth context).
305#[derive(Debug, Default, Clone, Copy)]
306pub struct AllowAllAuthProvider;
307
308impl AuthProvider for AllowAllAuthProvider {
309    fn authenticate(&self, _ctx: &McpContext, _request: AuthRequest<'_>) -> McpResult<AuthContext> {
310        Ok(AuthContext::anonymous())
311    }
312}
313
314// =============================================================================
315// Tests
316// =============================================================================
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use asupersync::Cx;
322
323    fn ctx() -> McpContext {
324        McpContext::new(Cx::for_testing(), 1)
325    }
326
327    #[test]
328    fn access_token_parse_accepts_bearer_and_bare_token() {
329        assert_eq!(
330            fastmcp_core::AccessToken::parse("Bearer abc"),
331            Some(fastmcp_core::AccessToken {
332                scheme: "Bearer".to_string(),
333                token: "abc".to_string(),
334            })
335        );
336        assert_eq!(
337            fastmcp_core::AccessToken::parse("abc"),
338            Some(fastmcp_core::AccessToken {
339                scheme: "Bearer".to_string(),
340                token: "abc".to_string(),
341            })
342        );
343        // Bare token parsing treats the entire value as a bearer token, even if it
344        // happens to be the literal string "Bearer".
345        assert_eq!(
346            fastmcp_core::AccessToken::parse(" Bearer"),
347            Some(fastmcp_core::AccessToken {
348                scheme: "Bearer".to_string(),
349                token: "Bearer".to_string(),
350            })
351        );
352        assert_eq!(fastmcp_core::AccessToken::parse(""), None);
353        assert_eq!(fastmcp_core::AccessToken::parse("   "), None);
354        assert_eq!(fastmcp_core::AccessToken::parse("Bearer "), None);
355    }
356
357    #[test]
358    fn auth_request_extracts_access_token_from_common_locations() {
359        // params as string
360        let req = AuthRequest {
361            method: "tools/call",
362            params: Some(&serde_json::Value::String("Bearer t1".to_string())),
363            request_id: 1,
364        };
365        assert_eq!(
366            req.access_token(),
367            Some(AccessToken {
368                scheme: "Bearer".to_string(),
369                token: "t1".to_string(),
370            })
371        );
372
373        // params as object with authorization field
374        let params = serde_json::json!({"authorization": "Bearer t2"});
375        let req = AuthRequest {
376            method: "tools/call",
377            params: Some(&params),
378            request_id: 1,
379        };
380        assert_eq!(
381            req.access_token(),
382            Some(AccessToken {
383                scheme: "Bearer".to_string(),
384                token: "t2".to_string(),
385            })
386        );
387
388        // params as object with {scheme, token}
389        let params = serde_json::json!({"auth": {"scheme": "Bearer", "token": "t3"}});
390        let req = AuthRequest {
391            method: "tools/call",
392            params: Some(&params),
393            request_id: 1,
394        };
395        assert_eq!(
396            req.access_token(),
397            Some(AccessToken {
398                scheme: "Bearer".to_string(),
399                token: "t3".to_string(),
400            })
401        );
402
403        // params as object with _meta.authorization
404        let params = serde_json::json!({"_meta": {"authorization": "Bearer t4"}});
405        let req = AuthRequest {
406            method: "tools/call",
407            params: Some(&params),
408            request_id: 1,
409        };
410        assert_eq!(
411            req.access_token(),
412            Some(AccessToken {
413                scheme: "Bearer".to_string(),
414                token: "t4".to_string(),
415            })
416        );
417
418        // params as object with headers.Authorization
419        let params = serde_json::json!({"headers": {"Authorization": "Bearer t5"}});
420        let req = AuthRequest {
421            method: "tools/call",
422            params: Some(&params),
423            request_id: 1,
424        };
425        assert_eq!(
426            req.access_token(),
427            Some(AccessToken {
428                scheme: "Bearer".to_string(),
429                token: "t5".to_string(),
430            })
431        );
432    }
433
434    #[test]
435    fn token_auth_provider_errors_on_missing_token_and_allows_override() {
436        #[derive(Debug)]
437        struct AcceptAll;
438        impl TokenVerifier for AcceptAll {
439            fn verify(
440                &self,
441                _ctx: &McpContext,
442                _request: AuthRequest<'_>,
443                _token: &AccessToken,
444            ) -> McpResult<AuthContext> {
445                Ok(AuthContext::with_subject("ok"))
446            }
447        }
448
449        let provider = TokenAuthProvider::new(AcceptAll);
450        let req = AuthRequest {
451            method: "tools/call",
452            params: None,
453            request_id: 1,
454        };
455        let err = provider.authenticate(&ctx(), req).unwrap_err();
456        assert_eq!(err.code, McpErrorCode::ResourceForbidden);
457        assert!(err.message.contains("Missing access token"));
458
459        let provider =
460            TokenAuthProvider::new(AcceptAll).with_missing_token_error(auth_error("no token"));
461        let req = AuthRequest {
462            method: "tools/call",
463            params: None,
464            request_id: 1,
465        };
466        let err = provider.authenticate(&ctx(), req).unwrap_err();
467        assert!(err.message.contains("no token"));
468    }
469
470    #[test]
471    fn static_token_verifier_enforces_scheme_and_sets_token_if_missing() {
472        let mut base = AuthContext::with_subject("user123");
473        base.scopes = vec!["read".to_string()];
474
475        let verifier =
476            StaticTokenVerifier::new([("value-1", base.clone())]).with_allowed_schemes(["Bearer"]);
477        let req = AuthRequest {
478            method: "tools/call",
479            params: None,
480            request_id: 1,
481        };
482
483        // Wrong scheme
484        let err = verifier
485            .verify(
486                &ctx(),
487                req,
488                &AccessToken {
489                    scheme: "Basic".to_string(),
490                    token: "value-1".to_string(),
491                },
492            )
493            .unwrap_err();
494        assert!(err.message.contains("Unsupported auth scheme"));
495
496        // Valid scheme (case-insensitive)
497        let auth = verifier
498            .verify(
499                &ctx(),
500                req,
501                &AccessToken {
502                    scheme: "bearer".to_string(),
503                    token: "value-1".to_string(),
504                },
505            )
506            .unwrap();
507        assert_eq!(auth.subject, Some("user123".to_string()));
508        assert_eq!(auth.scopes, vec!["read".to_string()]);
509        assert_eq!(
510            auth.token,
511            Some(AccessToken {
512                scheme: "bearer".to_string(),
513                token: "value-1".to_string(),
514            })
515        );
516
517        // If the stored context already has an access credential, keep it (do not override).
518        let stored_with_access = AuthContext {
519            token: Some(AccessToken {
520                scheme: "Bearer".to_string(),
521                token: "stored-value".to_string(),
522            }),
523            ..base.clone()
524        };
525        let verifier = StaticTokenVerifier::new([("value-2", stored_with_access)]);
526        let auth = verifier
527            .verify(
528                &ctx(),
529                req,
530                &AccessToken {
531                    scheme: "Bearer".to_string(),
532                    token: "value-2".to_string(),
533                },
534            )
535            .unwrap();
536        assert_eq!(
537            auth.token,
538            Some(AccessToken {
539                scheme: "Bearer".to_string(),
540                token: "stored-value".to_string(),
541            })
542        );
543    }
544
545    #[test]
546    fn allow_all_provider_returns_anonymous_context() {
547        let provider = AllowAllAuthProvider;
548        let req = AuthRequest {
549            method: "tools/call",
550            params: None,
551            request_id: 1,
552        };
553        let auth = provider.authenticate(&ctx(), req).unwrap();
554        assert_eq!(auth.subject, None);
555        assert!(auth.scopes.is_empty());
556    }
557
558    #[test]
559    fn access_token_from_none_params() {
560        let req = AuthRequest {
561            method: "tools/call",
562            params: None,
563            request_id: 1,
564        };
565        assert!(req.access_token().is_none());
566    }
567
568    #[test]
569    fn access_token_from_array_params() {
570        let params = serde_json::json!([1, 2, 3]);
571        let req = AuthRequest {
572            method: "tools/call",
573            params: Some(&params),
574            request_id: 1,
575        };
576        assert!(req.access_token().is_none());
577    }
578
579    #[test]
580    fn access_token_from_number_params() {
581        let params = serde_json::json!(42);
582        let req = AuthRequest {
583            method: "tools/call",
584            params: Some(&params),
585            request_id: 1,
586        };
587        assert!(req.access_token().is_none());
588    }
589
590    #[test]
591    fn access_token_from_object_with_token_field() {
592        let params = serde_json::json!({"token": "Bearer my-secret"});
593        let req = AuthRequest {
594            method: "tools/call",
595            params: Some(&params),
596            request_id: 1,
597        };
598        let token = req.access_token().expect("should extract token");
599        assert_eq!(token.scheme, "Bearer");
600        assert_eq!(token.token, "my-secret");
601    }
602
603    #[test]
604    fn access_token_from_object_with_access_token_field() {
605        let params = serde_json::json!({"access_token": "abc123"});
606        let req = AuthRequest {
607            method: "tools/call",
608            params: Some(&params),
609            request_id: 1,
610        };
611        let token = req.access_token().expect("should extract");
612        // Bare token defaults to Bearer scheme
613        assert_eq!(token.scheme, "Bearer");
614        assert_eq!(token.token, "abc123");
615    }
616
617    #[test]
618    fn access_token_from_camel_case_field() {
619        let params = serde_json::json!({"accessToken": "Bearer xyz"});
620        let req = AuthRequest {
621            method: "tools/call",
622            params: Some(&params),
623            request_id: 1,
624        };
625        let token = req.access_token().expect("should extract");
626        assert_eq!(token.token, "xyz");
627    }
628
629    #[test]
630    fn access_token_from_nested_scheme_token_object_with_empty_scheme() {
631        // Empty scheme falls through to the alternate path which finds "token" as a bare string
632        let params = serde_json::json!({"auth": {"scheme": "", "token": "abc"}});
633        let req = AuthRequest {
634            method: "tools/call",
635            params: Some(&params),
636            request_id: 1,
637        };
638        let token = req.access_token().expect("falls through to string parse");
639        assert_eq!(token.scheme, "Bearer");
640        assert_eq!(token.token, "abc");
641    }
642
643    #[test]
644    fn access_token_from_nested_scheme_token_object_with_whitespace_token() {
645        // Whitespace-only token should be rejected by the scheme/token path
646        // and also by AccessToken::parse which trims empty values
647        let params = serde_json::json!({"authorization": "  "});
648        let req = AuthRequest {
649            method: "tools/call",
650            params: Some(&params),
651            request_id: 1,
652        };
653        assert!(req.access_token().is_none());
654    }
655
656    #[test]
657    fn static_verifier_rejects_unknown_token() {
658        let verifier = StaticTokenVerifier::new([("valid-token", AuthContext::anonymous())]);
659        let req = AuthRequest {
660            method: "tools/call",
661            params: None,
662            request_id: 1,
663        };
664        let err = verifier
665            .verify(
666                &ctx(),
667                req,
668                &AccessToken {
669                    scheme: "Bearer".to_string(),
670                    token: "wrong-token".to_string(),
671                },
672            )
673            .unwrap_err();
674        assert_eq!(err.code, McpErrorCode::ResourceForbidden);
675        assert!(err.message.contains("Invalid access token"));
676    }
677
678    #[test]
679    fn static_verifier_no_scheme_restriction_allows_any() {
680        let verifier = StaticTokenVerifier::new([("tok", AuthContext::with_subject("alice"))]);
681        let req = AuthRequest {
682            method: "tools/call",
683            params: None,
684            request_id: 1,
685        };
686        let auth = verifier
687            .verify(
688                &ctx(),
689                req,
690                &AccessToken {
691                    scheme: "CustomScheme".to_string(),
692                    token: "tok".to_string(),
693                },
694            )
695            .unwrap();
696        assert_eq!(auth.subject, Some("alice".to_string()));
697    }
698
699    #[test]
700    fn token_auth_provider_succeeds_with_valid_token() {
701        let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("bob"))]);
702        let provider = TokenAuthProvider::new(verifier);
703        let params = serde_json::json!({"authorization": "Bearer secret"});
704        let req = AuthRequest {
705            method: "tools/call",
706            params: Some(&params),
707            request_id: 1,
708        };
709        let auth = provider.authenticate(&ctx(), req).unwrap();
710        assert_eq!(auth.subject, Some("bob".to_string()));
711    }
712
713    #[test]
714    fn token_auth_provider_fails_with_wrong_token() {
715        let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("bob"))]);
716        let provider = TokenAuthProvider::new(verifier);
717        let params = serde_json::json!({"authorization": "Bearer wrong"});
718        let req = AuthRequest {
719            method: "tools/call",
720            params: Some(&params),
721            request_id: 1,
722        };
723        let err = provider.authenticate(&ctx(), req).unwrap_err();
724        assert_eq!(err.code, McpErrorCode::ResourceForbidden);
725    }
726
727    #[test]
728    fn auth_request_debug() {
729        let params = serde_json::json!({"key": "val"});
730        let req = AuthRequest {
731            method: "test",
732            params: Some(&params),
733            request_id: 42,
734        };
735        let debug = format!("{req:?}");
736        assert!(debug.contains("test"));
737        assert!(debug.contains("42"));
738    }
739
740    #[test]
741    fn auth_request_clone_copy() {
742        let req = AuthRequest {
743            method: "test",
744            params: None,
745            request_id: 1,
746        };
747        let req2 = req; // Copy
748        assert_eq!(req.method, req2.method);
749        assert_eq!(req.request_id, req2.request_id);
750    }
751
752    #[test]
753    fn access_token_from_headers_nested_object() {
754        // headers containing an object with scheme and token
755        let params = serde_json::json!({
756            "headers": {
757                "Authorization": {"scheme": "Bearer", "token": "hdr-tok"}
758            }
759        });
760        let req = AuthRequest {
761            method: "tools/call",
762            params: Some(&params),
763            request_id: 1,
764        };
765        let token = req.access_token().expect("should extract from headers");
766        assert_eq!(token.scheme, "Bearer");
767        assert_eq!(token.token, "hdr-tok");
768    }
769
770    #[test]
771    fn access_token_from_empty_object() {
772        let params = serde_json::json!({});
773        let req = AuthRequest {
774            method: "tools/call",
775            params: Some(&params),
776            request_id: 1,
777        };
778        assert!(req.access_token().is_none());
779    }
780
781    // ── AllowAllAuthProvider derives ─────────────────────────────────
782
783    #[test]
784    fn allow_all_provider_debug() {
785        let provider = AllowAllAuthProvider;
786        let debug = format!("{provider:?}");
787        assert!(debug.contains("AllowAllAuthProvider"));
788    }
789
790    #[test]
791    fn allow_all_provider_default() {
792        let _ = AllowAllAuthProvider;
793    }
794
795    #[test]
796    fn allow_all_provider_clone_copy() {
797        let provider = AllowAllAuthProvider;
798        let cloned = provider.clone();
799        let copied = provider; // Copy
800        let _ = cloned
801            .authenticate(
802                &ctx(),
803                AuthRequest {
804                    method: "test",
805                    params: None,
806                    request_id: 1,
807                },
808            )
809            .unwrap();
810        let _ = copied;
811    }
812
813    // ── TokenAuthProvider ────────────────────────────────────────────
814
815    #[test]
816    fn token_auth_provider_clone() {
817        let verifier = StaticTokenVerifier::new([("tok", AuthContext::anonymous())]);
818        let provider = TokenAuthProvider::new(verifier);
819        let cloned = provider.clone();
820        let params = serde_json::json!({"authorization": "Bearer tok"});
821        let req = AuthRequest {
822            method: "tools/call",
823            params: Some(&params),
824            request_id: 1,
825        };
826        let auth = cloned.authenticate(&ctx(), req).unwrap();
827        assert!(auth.subject.is_none()); // anonymous
828    }
829
830    #[test]
831    fn token_auth_provider_with_custom_error_and_valid_token() {
832        let verifier = StaticTokenVerifier::new([("valid", AuthContext::with_subject("user"))]);
833        let provider =
834            TokenAuthProvider::new(verifier).with_missing_token_error(auth_error("custom missing"));
835        let params = serde_json::json!({"authorization": "Bearer valid"});
836        let req = AuthRequest {
837            method: "tools/call",
838            params: Some(&params),
839            request_id: 1,
840        };
841        let auth = provider.authenticate(&ctx(), req).unwrap();
842        assert_eq!(auth.subject, Some("user".to_string()));
843    }
844
845    // ── StaticTokenVerifier ──────────────────────────────────────────
846
847    #[test]
848    fn static_verifier_debug() {
849        let verifier = StaticTokenVerifier::new([("tok", AuthContext::anonymous())]);
850        let debug = format!("{verifier:?}");
851        assert!(debug.contains("StaticTokenVerifier"));
852    }
853
854    #[test]
855    fn static_verifier_clone() {
856        let verifier = StaticTokenVerifier::new([("tok", AuthContext::with_subject("a"))]);
857        let cloned = verifier.clone();
858        let req = AuthRequest {
859            method: "test",
860            params: None,
861            request_id: 1,
862        };
863        let auth = cloned
864            .verify(
865                &ctx(),
866                req,
867                &AccessToken {
868                    scheme: "Bearer".to_string(),
869                    token: "tok".to_string(),
870                },
871            )
872            .unwrap();
873        assert_eq!(auth.subject, Some("a".to_string()));
874    }
875
876    #[test]
877    fn static_verifier_multiple_tokens() {
878        let verifier = StaticTokenVerifier::new([
879            ("alpha", AuthContext::with_subject("alice")),
880            ("beta", AuthContext::with_subject("bob")),
881        ]);
882        let req = AuthRequest {
883            method: "test",
884            params: None,
885            request_id: 1,
886        };
887        let a = verifier
888            .verify(
889                &ctx(),
890                req,
891                &AccessToken {
892                    scheme: "Bearer".to_string(),
893                    token: "alpha".to_string(),
894                },
895            )
896            .unwrap();
897        assert_eq!(a.subject, Some("alice".to_string()));
898        let b = verifier
899            .verify(
900                &ctx(),
901                req,
902                &AccessToken {
903                    scheme: "Bearer".to_string(),
904                    token: "beta".to_string(),
905                },
906            )
907            .unwrap();
908        assert_eq!(b.subject, Some("bob".to_string()));
909    }
910
911    #[test]
912    fn static_verifier_multiple_allowed_schemes() {
913        let verifier = StaticTokenVerifier::new([("tok", AuthContext::anonymous())])
914            .with_allowed_schemes(["Bearer", "Token"]);
915        let req = AuthRequest {
916            method: "test",
917            params: None,
918            request_id: 1,
919        };
920        // Bearer works
921        assert!(
922            verifier
923                .verify(
924                    &ctx(),
925                    req,
926                    &AccessToken {
927                        scheme: "Bearer".to_string(),
928                        token: "tok".to_string(),
929                    },
930                )
931                .is_ok()
932        );
933        // Token works
934        assert!(
935            verifier
936                .verify(
937                    &ctx(),
938                    req,
939                    &AccessToken {
940                        scheme: "Token".to_string(),
941                        token: "tok".to_string(),
942                    },
943                )
944                .is_ok()
945        );
946        // Basic does not
947        assert!(
948            verifier
949                .verify(
950                    &ctx(),
951                    req,
952                    &AccessToken {
953                        scheme: "Basic".to_string(),
954                        token: "tok".to_string(),
955                    },
956                )
957                .is_err()
958        );
959    }
960
961    // ── extract_from_value edge cases ────────────────────────────────
962
963    #[test]
964    fn access_token_from_bool_value_returns_none() {
965        let params = serde_json::json!(true);
966        let req = AuthRequest {
967            method: "test",
968            params: Some(&params),
969            request_id: 1,
970        };
971        assert!(req.access_token().is_none());
972    }
973
974    #[test]
975    fn access_token_from_null_params() {
976        let params = serde_json::json!(null);
977        let req = AuthRequest {
978            method: "test",
979            params: Some(&params),
980            request_id: 1,
981        };
982        assert!(req.access_token().is_none());
983    }
984
985    #[test]
986    fn access_token_from_nested_object_with_inner_authorization_string() {
987        // Object with auth field pointing to object that has an inner authorization string
988        let params = serde_json::json!({
989            "auth": {
990                "authorization": "Bearer inner-tok"
991            }
992        });
993        let req = AuthRequest {
994            method: "test",
995            params: Some(&params),
996            request_id: 1,
997        };
998        let token = req.access_token().expect("should extract from nested auth");
999        assert_eq!(token.token, "inner-tok");
1000    }
1001
1002    // ── _meta and headers fallback priority ──────────────────────────
1003
1004    #[test]
1005    fn access_token_meta_fallback_when_top_level_empty() {
1006        let params = serde_json::json!({
1007            "other_field": 123,
1008            "_meta": {"authorization": "Bearer meta-tok"}
1009        });
1010        let req = AuthRequest {
1011            method: "test",
1012            params: Some(&params),
1013            request_id: 1,
1014        };
1015        let token = req.access_token().expect("should fallback to _meta");
1016        assert_eq!(token.token, "meta-tok");
1017    }
1018
1019    #[test]
1020    fn access_token_headers_fallback_when_top_and_meta_empty() {
1021        let params = serde_json::json!({
1022            "other": "value",
1023            "_meta": {"other": "value"},
1024            "headers": {"authorization": "Bearer hdr-tok"}
1025        });
1026        let req = AuthRequest {
1027            method: "test",
1028            params: Some(&params),
1029            request_id: 1,
1030        };
1031        let token = req.access_token().expect("should fallback to headers");
1032        assert_eq!(token.token, "hdr-tok");
1033    }
1034
1035    #[test]
1036    fn access_token_top_level_wins_over_meta() {
1037        let params = serde_json::json!({
1038            "authorization": "Bearer top-tok",
1039            "_meta": {"authorization": "Bearer meta-tok"}
1040        });
1041        let req = AuthRequest {
1042            method: "test",
1043            params: Some(&params),
1044            request_id: 1,
1045        };
1046        let token = req.access_token().expect("should extract top-level");
1047        assert_eq!(token.token, "top-tok");
1048    }
1049
1050    // ── auth_error helper ────────────────────────────────────────────
1051
1052    #[test]
1053    fn auth_error_creates_resource_forbidden() {
1054        let err = auth_error("denied");
1055        assert_eq!(err.code, McpErrorCode::ResourceForbidden);
1056        assert!(err.message.contains("denied"));
1057    }
1058
1059    // ── extract_from_value with non-matching object ──────────────────
1060
1061    #[test]
1062    fn access_token_from_object_without_any_known_key() {
1063        let params = serde_json::json!({"unknown_key": "Bearer tok"});
1064        let req = AuthRequest {
1065            method: "test",
1066            params: Some(&params),
1067            request_id: 1,
1068        };
1069        assert!(req.access_token().is_none());
1070    }
1071
1072    #[test]
1073    fn access_token_from_scheme_token_with_whitespace_only_scheme() {
1074        let params = serde_json::json!({"auth": {"scheme": "  ", "token": "abc"}});
1075        let req = AuthRequest {
1076            method: "test",
1077            params: Some(&params),
1078            request_id: 1,
1079        };
1080        // Whitespace scheme is rejected, falls through to find "token" key in nested object
1081        let token = req.access_token().expect("falls through to token key");
1082        assert_eq!(token.scheme, "Bearer");
1083        assert_eq!(token.token, "abc");
1084    }
1085
1086    // ── _meta / headers non-object fallthrough ──────────────────────
1087
1088    #[test]
1089    fn access_token_meta_non_object_falls_through_to_headers() {
1090        let params = serde_json::json!({
1091            "_meta": 42,
1092            "headers": {"authorization": "Bearer hdr"}
1093        });
1094        let req = AuthRequest {
1095            method: "test",
1096            params: Some(&params),
1097            request_id: 1,
1098        };
1099        let token = req.access_token().expect("should skip non-object _meta");
1100        assert_eq!(token.token, "hdr");
1101    }
1102
1103    #[test]
1104    fn access_token_headers_non_object_returns_none() {
1105        let params = serde_json::json!({
1106            "_meta": {"other": true},
1107            "headers": "not-an-object"
1108        });
1109        let req = AuthRequest {
1110            method: "test",
1111            params: Some(&params),
1112            request_id: 1,
1113        };
1114        assert!(req.access_token().is_none());
1115    }
1116
1117    // ── Non-string, non-object values in map fields ─────────────────
1118
1119    #[test]
1120    fn access_token_map_field_with_numeric_value_returns_none() {
1121        let params = serde_json::json!({"authorization": 12345});
1122        let req = AuthRequest {
1123            method: "test",
1124            params: Some(&params),
1125            request_id: 1,
1126        };
1127        assert!(req.access_token().is_none());
1128    }
1129
1130    #[test]
1131    fn access_token_map_field_with_bool_value_returns_none() {
1132        let params = serde_json::json!({"token": true});
1133        let req = AuthRequest {
1134            method: "test",
1135            params: Some(&params),
1136            request_id: 1,
1137        };
1138        assert!(req.access_token().is_none());
1139    }
1140
1141    #[test]
1142    fn access_token_map_field_with_array_value_returns_none() {
1143        let params = serde_json::json!({"authorization": ["Bearer", "tok"]});
1144        let req = AuthRequest {
1145            method: "test",
1146            params: Some(&params),
1147            request_id: 1,
1148        };
1149        assert!(req.access_token().is_none());
1150    }
1151
1152    // ── extract_from_value nested accessToken key ───────────────────
1153
1154    #[test]
1155    fn access_token_nested_object_with_access_token_key() {
1156        let params = serde_json::json!({
1157            "auth": {
1158                "accessToken": "Bearer nested-at"
1159            }
1160        });
1161        let req = AuthRequest {
1162            method: "test",
1163            params: Some(&params),
1164            request_id: 1,
1165        };
1166        let token = req
1167            .access_token()
1168            .expect("should extract from nested accessToken");
1169        assert_eq!(token.token, "nested-at");
1170    }
1171
1172    // ── StaticTokenVerifier with empty allowed_schemes ──────────────
1173
1174    #[test]
1175    fn static_verifier_empty_allowed_schemes_rejects_all() {
1176        let verifier = StaticTokenVerifier::new([("tok", AuthContext::anonymous())])
1177            .with_allowed_schemes(Vec::<String>::new());
1178        let req = AuthRequest {
1179            method: "test",
1180            params: None,
1181            request_id: 1,
1182        };
1183        let err = verifier
1184            .verify(
1185                &ctx(),
1186                req,
1187                &AccessToken {
1188                    scheme: "Bearer".to_string(),
1189                    token: "tok".to_string(),
1190                },
1191            )
1192            .unwrap_err();
1193        assert!(err.message.contains("Unsupported auth scheme"));
1194    }
1195
1196    // ── TokenAuthProvider with scheme restriction in verifier ────────
1197
1198    #[test]
1199    fn token_auth_provider_with_scheme_restriction() {
1200        let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("user"))])
1201            .with_allowed_schemes(["Bearer"]);
1202        let provider = TokenAuthProvider::new(verifier);
1203
1204        // Basic scheme rejected by verifier
1205        let params = serde_json::json!({"authorization": "Basic secret"});
1206        let req = AuthRequest {
1207            method: "test",
1208            params: Some(&params),
1209            request_id: 1,
1210        };
1211        let err = provider.authenticate(&ctx(), req).unwrap_err();
1212        assert!(err.message.contains("Unsupported"));
1213
1214        // Bearer scheme accepted
1215        let params = serde_json::json!({"authorization": "Bearer secret"});
1216        let req = AuthRequest {
1217            method: "test",
1218            params: Some(&params),
1219            request_id: 1,
1220        };
1221        let auth = provider.authenticate(&ctx(), req).unwrap();
1222        assert_eq!(auth.subject, Some("user".to_string()));
1223    }
1224
1225    // ── AuthRequest with all fields populated ───────────────────────
1226
1227    #[test]
1228    fn auth_request_exposes_all_fields() {
1229        let params = serde_json::json!({"key": "val"});
1230        let req = AuthRequest {
1231            method: "prompts/get",
1232            params: Some(&params),
1233            request_id: 99,
1234        };
1235        assert_eq!(req.method, "prompts/get");
1236        assert_eq!(req.request_id, 99);
1237        assert!(req.params.is_some());
1238    }
1239}
1240
1241#[cfg(all(test, feature = "jwt"))]
1242mod jwt_tests {
1243    use super::*;
1244    use asupersync::Cx;
1245    use jsonwebtoken::{EncodingKey, Header, encode};
1246
1247    fn ctx() -> McpContext {
1248        McpContext::new(Cx::for_testing(), 1)
1249    }
1250
1251    fn hs256_token(signing_bytes: &[u8], claims: serde_json::Value) -> String {
1252        encode(
1253            &Header::new(jsonwebtoken::Algorithm::HS256),
1254            &claims,
1255            &EncodingKey::from_secret(signing_bytes),
1256        )
1257        .expect("encode jwt")
1258    }
1259
1260    #[test]
1261    fn jwt_token_verifier_extracts_subject_and_scopes() {
1262        let signing_bytes = b"test-hs256-bytes";
1263        let exp = (chrono::Utc::now() + chrono::Duration::minutes(10)).timestamp();
1264        let jwt = hs256_token(
1265            signing_bytes,
1266            serde_json::json!({
1267                "sub": "user123",
1268                "scope": "openid profile",
1269                "scopes": ["email"],
1270                "exp": exp,
1271            }),
1272        );
1273
1274        let verifier = JwtTokenVerifier::hs256(signing_bytes);
1275        let req = AuthRequest {
1276            method: "initialize",
1277            params: None,
1278            request_id: 1,
1279        };
1280
1281        let access = AccessToken {
1282            scheme: "Bearer".to_string(),
1283            token: jwt,
1284        };
1285        let auth = verifier.verify(&ctx(), req, &access).unwrap();
1286        assert_eq!(auth.subject, Some("user123".to_string()));
1287        assert_eq!(
1288            auth.scopes,
1289            vec![
1290                "openid".to_string(),
1291                "profile".to_string(),
1292                "email".to_string()
1293            ]
1294        );
1295        assert!(auth.claims.is_some());
1296        assert!(auth.token.is_some());
1297    }
1298
1299    #[test]
1300    fn jwt_token_verifier_rejects_wrong_scheme_and_invalid_token() {
1301        let signing_bytes = b"test-hs256-bytes";
1302        let exp = (chrono::Utc::now() + chrono::Duration::minutes(10)).timestamp();
1303        let jwt = hs256_token(
1304            signing_bytes,
1305            serde_json::json!({
1306                "sub": "user123",
1307                "exp": exp,
1308            }),
1309        );
1310
1311        let verifier = JwtTokenVerifier::hs256(signing_bytes);
1312        let req = AuthRequest {
1313            method: "initialize",
1314            params: None,
1315            request_id: 1,
1316        };
1317
1318        // Wrong scheme
1319        let err = verifier
1320            .verify(
1321                &ctx(),
1322                req,
1323                &AccessToken {
1324                    scheme: "Basic".to_string(),
1325                    token: jwt.clone(),
1326                },
1327            )
1328            .unwrap_err();
1329        assert_eq!(err.code, McpErrorCode::ResourceForbidden);
1330        assert!(err.message.contains("Unsupported auth scheme"));
1331
1332        // Invalid token (wrong signing bytes)
1333        let bad = hs256_token(
1334            b"other-hs256-bytes",
1335            serde_json::json!({
1336                "sub": "user123",
1337                "exp": exp,
1338            }),
1339        );
1340        let err = verifier
1341            .verify(
1342                &ctx(),
1343                req,
1344                &AccessToken {
1345                    scheme: "Bearer".to_string(),
1346                    token: bad,
1347                },
1348            )
1349            .unwrap_err();
1350        assert!(err.message.contains("Invalid token"));
1351    }
1352}