1use std::collections::HashMap;
8use std::sync::Arc;
9
10use fastmcp_core::{AccessToken, AuthContext, McpContext, McpError, McpErrorCode, McpResult};
11
12#[derive(Debug, Clone, Copy)]
14pub struct AuthRequest<'a> {
15 pub method: &'a str,
17 pub params: Option<&'a serde_json::Value>,
19 pub request_id: u64,
21}
22
23impl AuthRequest<'_> {
24 #[must_use]
26 pub fn access_token(&self) -> Option<AccessToken> {
27 extract_access_token(self.params)
28 }
29}
30
31fn 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
102pub trait AuthProvider: Send + Sync {
107 fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext>;
111}
112
113pub trait TokenVerifier: Send + Sync {
115 fn verify(
117 &self,
118 ctx: &McpContext,
119 request: AuthRequest<'_>,
120 token: &AccessToken,
121 ) -> McpResult<AuthContext>;
122}
123
124#[derive(Clone)]
126pub struct TokenAuthProvider {
127 verifier: Arc<dyn TokenVerifier>,
128 missing_token_error: McpError,
129}
130
131impl TokenAuthProvider {
132 #[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 #[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#[derive(Debug, Clone)]
160pub struct StaticTokenVerifier {
161 tokens: HashMap<String, AuthContext>,
162 allowed_schemes: Option<Vec<String>>,
163}
164
165impl StaticTokenVerifier {
166 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 #[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 #[derive(Debug, Clone)]
232 pub struct JwtTokenVerifier {
233 decoding_key: DecodingKey,
234 validation: Validation,
235 }
236
237 impl JwtTokenVerifier {
238 #[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 #[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#[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#[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 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 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 let params = serde_json::json!({"authorization": "Bearer t2"});
375 let req = AuthRequest {
376 method: "tools/call",
377 params: Some(¶ms),
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 let params = serde_json::json!({"auth": {"scheme": "Bearer", "token": "t3"}});
390 let req = AuthRequest {
391 method: "tools/call",
392 params: Some(¶ms),
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 let params = serde_json::json!({"_meta": {"authorization": "Bearer t4"}});
405 let req = AuthRequest {
406 method: "tools/call",
407 params: Some(¶ms),
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 let params = serde_json::json!({"headers": {"Authorization": "Bearer t5"}});
420 let req = AuthRequest {
421 method: "tools/call",
422 params: Some(¶ms),
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 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 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 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(¶ms),
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(¶ms),
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(¶ms),
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(¶ms),
609 request_id: 1,
610 };
611 let token = req.access_token().expect("should extract");
612 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(¶ms),
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 let params = serde_json::json!({"auth": {"scheme": "", "token": "abc"}});
633 let req = AuthRequest {
634 method: "tools/call",
635 params: Some(¶ms),
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 let params = serde_json::json!({"authorization": " "});
648 let req = AuthRequest {
649 method: "tools/call",
650 params: Some(¶ms),
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(¶ms),
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(¶ms),
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(¶ms),
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; 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 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(¶ms),
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(¶ms),
776 request_id: 1,
777 };
778 assert!(req.access_token().is_none());
779 }
780
781 #[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; 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 #[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(¶ms),
824 request_id: 1,
825 };
826 let auth = cloned.authenticate(&ctx(), req).unwrap();
827 assert!(auth.subject.is_none()); }
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(¶ms),
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 #[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 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 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 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 #[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(¶ms),
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(¶ms),
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 let params = serde_json::json!({
989 "auth": {
990 "authorization": "Bearer inner-tok"
991 }
992 });
993 let req = AuthRequest {
994 method: "test",
995 params: Some(¶ms),
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 #[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(¶ms),
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(¶ms),
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(¶ms),
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 #[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 #[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(¶ms),
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(¶ms),
1078 request_id: 1,
1079 };
1080 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 #[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(¶ms),
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(¶ms),
1112 request_id: 1,
1113 };
1114 assert!(req.access_token().is_none());
1115 }
1116
1117 #[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(¶ms),
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(¶ms),
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(¶ms),
1147 request_id: 1,
1148 };
1149 assert!(req.access_token().is_none());
1150 }
1151
1152 #[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(¶ms),
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 #[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 #[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 let params = serde_json::json!({"authorization": "Basic secret"});
1206 let req = AuthRequest {
1207 method: "test",
1208 params: Some(¶ms),
1209 request_id: 1,
1210 };
1211 let err = provider.authenticate(&ctx(), req).unwrap_err();
1212 assert!(err.message.contains("Unsupported"));
1213
1214 let params = serde_json::json!({"authorization": "Bearer secret"});
1216 let req = AuthRequest {
1217 method: "test",
1218 params: Some(¶ms),
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 #[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(¶ms),
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 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 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}