1use std::collections::HashMap;
9use std::io::Write as _;
10use std::sync::Arc;
11
12use fastmcp_core::{
13 AccessToken, AuthContext, MAX_ACCESS_TOKEN_BYTES, McpContext, McpError, McpErrorCode,
14 McpResult, Sha256Digest, sha256_bounded,
15};
16
17const ACCESS_TOKEN_FIELDS: [&str; 6] = [
18 "authorization",
19 "Authorization",
20 "auth",
21 "token",
22 "access_token",
23 "accessToken",
24];
25
26const MAX_AUTH_SUBJECT_BYTES: usize = 1024;
27const MAX_AUTH_SCOPES: usize = 64;
28const MAX_AUTH_SCOPE_BYTES: usize = 256;
29const MAX_AUTH_CLAIM_NODES: usize = 1024;
30const MAX_AUTH_CLAIM_DEPTH: usize = 32;
31const MAX_AUTH_CLAIM_STRING_BYTES: usize = 16 * 1024;
32const MAX_AUTH_CONTEXT_BYTES: usize = 64 * 1024;
33const AUTH_BYTES_GROWTH_CHUNK: usize = 4 * 1024;
34const MAX_STATIC_TOKEN_ENTRIES: usize = 4_096;
35const MAX_ALLOWED_AUTH_SCHEMES: usize = 16;
36const AUTHENTICATED_PRINCIPAL_DOMAIN: &[u8] = b"fastmcp/session-principal/authenticated/v1\0";
37const ANONYMOUS_PRINCIPAL_DOMAIN: &[u8] = b"fastmcp/session-principal/anonymous/v1\0";
38
39struct BoundedAuthBytes {
40 bytes: Vec<u8>,
41}
42
43impl BoundedAuthBytes {
44 fn new() -> Self {
45 Self { bytes: Vec::new() }
46 }
47
48 fn ensure_capacity_for(&mut self, next_size: usize) -> std::io::Result<()> {
49 if next_size <= self.bytes.capacity() {
50 return Ok(());
51 }
52
53 let current_capacity = self.bytes.capacity();
54 let geometric_target = if current_capacity == 0 {
55 AUTH_BYTES_GROWTH_CHUNK
56 } else {
57 current_capacity
58 .checked_mul(2)
59 .unwrap_or(MAX_AUTH_CONTEXT_BYTES)
60 .min(MAX_AUTH_CONTEXT_BYTES)
61 };
62 let target_capacity = next_size.max(geometric_target).min(MAX_AUTH_CONTEXT_BYTES);
63
64 let mut grown = Vec::new();
68 grown
69 .try_reserve_exact(target_capacity)
70 .map_err(|_| std::io::Error::other("authentication context allocation failed"))?;
71 if grown.capacity() > MAX_AUTH_CONTEXT_BYTES {
72 return Err(std::io::Error::other(
73 "authentication context allocation exceeds byte limit",
74 ));
75 }
76 grown.extend_from_slice(&self.bytes);
77 self.bytes = grown;
78 Ok(())
79 }
80}
81
82impl std::io::Write for BoundedAuthBytes {
83 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
84 let new_len = self
85 .bytes
86 .len()
87 .checked_add(buffer.len())
88 .filter(|length| *length <= MAX_AUTH_CONTEXT_BYTES)
89 .ok_or_else(|| std::io::Error::other("authentication context exceeds byte limit"))?;
90 self.ensure_capacity_for(new_len)?;
91 self.bytes.extend_from_slice(buffer);
92 Ok(buffer.len())
93 }
94
95 fn flush(&mut self) -> std::io::Result<()> {
96 Ok(())
97 }
98}
99
100fn claims_shape_is_bounded(root: &serde_json::Value) -> bool {
101 let mut stack = Vec::new();
102 if stack.try_reserve_exact(1).is_err() {
103 return false;
104 }
105 stack.push((root, 0_usize));
106 let mut nodes = 0_usize;
107
108 while let Some((value, depth)) = stack.pop() {
109 nodes = match nodes.checked_add(1) {
110 Some(nodes) if nodes <= MAX_AUTH_CLAIM_NODES => nodes,
111 _ => return false,
112 };
113 if depth > MAX_AUTH_CLAIM_DEPTH {
114 return false;
115 }
116 match value {
117 serde_json::Value::String(value) => {
118 if value.len() > MAX_AUTH_CLAIM_STRING_BYTES {
119 return false;
120 }
121 }
122 serde_json::Value::Array(values) => {
123 for value in values {
124 if nodes.saturating_add(stack.len()) >= MAX_AUTH_CLAIM_NODES
125 || stack.try_reserve(1).is_err()
126 {
127 return false;
128 }
129 stack.push((value, depth.saturating_add(1)));
130 }
131 }
132 serde_json::Value::Object(values) => {
133 for (key, value) in values {
134 if key.len() > MAX_AUTH_CLAIM_STRING_BYTES
135 || nodes.saturating_add(stack.len()) >= MAX_AUTH_CLAIM_NODES
136 || stack.try_reserve(1).is_err()
137 {
138 return false;
139 }
140 stack.push((value, depth.saturating_add(1)));
141 }
142 }
143 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {
144 }
145 }
146 }
147 true
148}
149
150pub(crate) fn principal_fingerprint(auth: Option<&AuthContext>) -> McpResult<Sha256Digest> {
151 let mut canonical = BoundedAuthBytes::new();
152 match auth {
153 None => canonical
154 .write_all(ANONYMOUS_PRINCIPAL_DOMAIN)
155 .map_err(|_| McpError::internal_error("authentication admission exceeds bounds"))?,
156 Some(auth) => {
157 if auth
158 .subject
159 .as_ref()
160 .is_some_and(|subject| subject.is_empty() || subject.len() > MAX_AUTH_SUBJECT_BYTES)
161 || auth.scopes.len() > MAX_AUTH_SCOPES
162 || auth
163 .scopes
164 .iter()
165 .any(|scope| scope.is_empty() || scope.len() > MAX_AUTH_SCOPE_BYTES)
166 || auth
167 .claims
168 .as_ref()
169 .is_some_and(|claims| !claims_shape_is_bounded(claims))
170 {
171 return Err(McpError::internal_error(
172 "authentication provider returned facts outside admission bounds",
173 ));
174 }
175 let mut admitted_facts = BoundedAuthBytes::new();
176 serde_json::to_writer(&mut admitted_facts, auth).map_err(|_| {
177 McpError::internal_error(
178 "authentication provider returned facts outside admission bounds",
179 )
180 })?;
181 canonical
182 .write_all(AUTHENTICATED_PRINCIPAL_DOMAIN)
183 .map_err(|_| McpError::internal_error("authentication admission exceeds bounds"))?;
184 match (auth.session_owner(), auth.subject.as_deref()) {
185 (Some(owner), _) => {
186 canonical.write_all(&[2]).map_err(|_| {
187 McpError::internal_error("authentication admission exceeds bounds")
188 })?;
189 canonical.write_all(owner.as_bytes()).map_err(|_| {
190 McpError::internal_error("authentication admission exceeds bounds")
191 })?;
192 }
193 (None, None) if auth.scopes.is_empty() && auth.claims.is_none() => {
194 canonical.write_all(&[0]).map_err(|_| {
195 McpError::internal_error("authentication admission exceeds bounds")
196 })?;
197 }
198 (None, None) => {
199 return Err(McpError::internal_error(
200 "authentication provider returned ownerless authorization facts",
201 ));
202 }
203 (None, Some(subject)) => {
204 canonical.write_all(&[1]).map_err(|_| {
205 McpError::internal_error("authentication admission exceeds bounds")
206 })?;
207 let length = u64::try_from(subject.len()).map_err(|_| {
208 McpError::internal_error("authentication admission exceeds bounds")
209 })?;
210 canonical.write_all(&length.to_be_bytes()).map_err(|_| {
211 McpError::internal_error("authentication admission exceeds bounds")
212 })?;
213 canonical.write_all(subject.as_bytes()).map_err(|_| {
214 McpError::internal_error("authentication admission exceeds bounds")
215 })?;
216 }
217 }
218 }
219 }
220 sha256_bounded(&canonical.bytes, MAX_AUTH_CONTEXT_BYTES)
221 .map_err(|_| McpError::internal_error("authentication admission exceeds bounds"))
222}
223
224#[derive(Clone, Copy)]
226pub struct AuthRequest<'a> {
227 pub method: &'a str,
229 pub params: Option<&'a serde_json::Value>,
237 pub transport_authorization: Option<&'a str>,
243 pub request_id: u64,
245}
246
247impl std::fmt::Debug for AuthRequest<'_> {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("AuthRequest")
250 .field("method_bytes", &self.method.len())
251 .field("params_present", &self.params.is_some())
252 .field(
253 "transport_authorization_present",
254 &self.transport_authorization.is_some(),
255 )
256 .field("request_id", &self.request_id)
257 .finish()
258 }
259}
260
261impl AuthRequest<'_> {
262 #[must_use]
267 pub fn access_token(&self) -> Option<AccessToken> {
268 admitted_access_token(*self).ok().flatten()
269 }
270
271 #[must_use]
274 pub fn has_multiple_credential_sources(&self) -> bool {
275 matches!(
276 admitted_access_token(*self),
277 Err(CredentialSourceError::Multiple)
278 )
279 }
280
281 pub(crate) fn credential_sources_are_admissible(&self) -> bool {
282 admitted_access_token(*self).is_ok()
283 }
284
285 pub(crate) fn has_any_credential_source(&self) -> bool {
286 self.transport_authorization.is_some() || single_in_band_credential(self.params).is_some()
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291enum CredentialSourceError {
292 Malformed,
293 Multiple,
294}
295
296fn admitted_access_token(
297 request: AuthRequest<'_>,
298) -> Result<Option<AccessToken>, CredentialSourceError> {
299 let in_band = single_in_band_credential(request.params);
300 match request.transport_authorization {
301 Some(_) if in_band.is_some() => Err(CredentialSourceError::Multiple),
302 Some(authorization) => parse_native_authorization(authorization)
303 .map(Some)
304 .ok_or(CredentialSourceError::Malformed),
305 None => match in_band.transpose()? {
306 Some(value) => extract_from_value(value).map(Some),
307 None => Ok(None),
308 },
309 }
310}
311
312fn record_credential_candidate<'a>(
313 candidate: &mut Option<&'a serde_json::Value>,
314 value: &'a serde_json::Value,
315) -> Result<(), CredentialSourceError> {
316 if candidate.replace(value).is_some() {
317 return Err(CredentialSourceError::Multiple);
318 }
319 Ok(())
320}
321
322fn scan_credential_map<'a>(
323 map: &'a serde_json::Map<String, serde_json::Value>,
324 candidate: &mut Option<&'a serde_json::Value>,
325) -> Result<(), CredentialSourceError> {
326 for key in ACCESS_TOKEN_FIELDS {
327 if let Some(value) = map.get(key) {
328 record_credential_candidate(candidate, value)?;
329 }
330 }
331 Ok(())
332}
333
334fn single_in_band_credential(
335 params: Option<&serde_json::Value>,
336) -> Option<Result<&serde_json::Value, CredentialSourceError>> {
337 let params = params?;
338 if matches!(params, serde_json::Value::String(_)) {
339 return Some(Ok(params));
340 }
341 let serde_json::Value::Object(map) = params else {
342 return None;
343 };
344
345 let mut candidate = None;
346 if let Err(error) = scan_credential_map(map, &mut candidate) {
347 return Some(Err(error));
348 }
349 for container in ["_meta", "headers"] {
350 if let Some(nested) = map.get(container).and_then(serde_json::Value::as_object)
351 && let Err(error) = scan_credential_map(nested, &mut candidate)
352 {
353 return Some(Err(error));
354 }
355 }
356 candidate.map(Ok)
357}
358
359fn parse_native_authorization(value: &str) -> Option<AccessToken> {
360 AccessToken::parse(value)
361}
362
363fn extract_from_value(value: &serde_json::Value) -> Result<AccessToken, CredentialSourceError> {
364 match value {
365 serde_json::Value::String(value) => {
366 AccessToken::parse_legacy_in_band(value).ok_or(CredentialSourceError::Malformed)
367 }
368 serde_json::Value::Object(map) => {
369 let scheme = map.get("scheme");
370 let token = map.get("token");
371 let alternative_keys = [
372 "authorization",
373 "Authorization",
374 "access_token",
375 "accessToken",
376 ];
377 let alternative_count = alternative_keys
378 .iter()
379 .filter(|key| map.contains_key(**key))
380 .count();
381
382 if scheme.is_some() || token.is_some() {
383 if alternative_count != 0 {
384 return Err(CredentialSourceError::Multiple);
385 }
386 let scheme = scheme
387 .and_then(serde_json::Value::as_str)
388 .ok_or(CredentialSourceError::Malformed)?;
389 let token = token
390 .and_then(serde_json::Value::as_str)
391 .ok_or(CredentialSourceError::Malformed)?;
392 let access = AccessToken::from_parts(scheme, token)
393 .ok_or(CredentialSourceError::Malformed)?;
394 return (access.scheme == scheme && access.token == token)
395 .then_some(access)
396 .ok_or(CredentialSourceError::Malformed);
397 }
398
399 if alternative_count > 1 {
400 return Err(CredentialSourceError::Multiple);
401 }
402 if alternative_count == 0 {
403 return Err(CredentialSourceError::Malformed);
404 }
405 let value = alternative_keys
406 .iter()
407 .find_map(|key| map.get(*key))
408 .and_then(serde_json::Value::as_str)
409 .ok_or(CredentialSourceError::Malformed)?;
410 AccessToken::parse_legacy_in_band(value).ok_or(CredentialSourceError::Malformed)
411 }
412 _ => Err(CredentialSourceError::Malformed),
413 }
414}
415
416pub(crate) fn strip_recognized_access_credentials(params: &mut Option<serde_json::Value>) {
424 if matches!(params, Some(serde_json::Value::String(_))) {
427 *params = None;
428 return;
429 }
430
431 let Some(serde_json::Value::Object(map)) = params.as_mut() else {
432 return;
433 };
434 remove_access_token_fields(map);
435 for container in ["_meta", "headers"] {
436 if let Some(nested) = map
437 .get_mut(container)
438 .and_then(serde_json::Value::as_object_mut)
439 {
440 remove_access_token_fields(nested);
441 }
442 }
443}
444
445fn remove_access_token_fields(map: &mut serde_json::Map<String, serde_json::Value>) {
446 for key in ACCESS_TOKEN_FIELDS {
447 map.remove(key);
448 }
449}
450
451pub trait AuthProvider: Send + Sync {
456 fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext>;
466}
467
468pub trait TokenVerifier: Send + Sync {
470 fn verify(
472 &self,
473 ctx: &McpContext,
474 request: AuthRequest<'_>,
475 token: &AccessToken,
476 ) -> McpResult<AuthContext>;
477}
478
479#[derive(Clone)]
481pub struct TokenAuthProvider {
482 verifier: Arc<dyn TokenVerifier>,
483 missing_token_error: McpError,
484}
485
486impl TokenAuthProvider {
487 #[must_use]
489 pub fn new<V: TokenVerifier + 'static>(verifier: V) -> Self {
490 Self {
491 verifier: Arc::new(verifier),
492 missing_token_error: auth_error("Missing access token"),
493 }
494 }
495
496 #[must_use]
498 pub fn with_missing_token_error(mut self, error: McpError) -> Self {
499 self.missing_token_error = error;
500 self
501 }
502}
503
504impl AuthProvider for TokenAuthProvider {
505 fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext> {
506 let access = request
507 .access_token()
508 .ok_or_else(|| self.missing_token_error.clone())?;
509 self.verifier.verify(ctx, request, &access)
510 }
511}
512
513pub struct StaticTokenVerifier {
515 tokens: HashMap<Sha256Digest, AuthContext>,
516 allowed_schemes: Option<Vec<String>>,
517}
518
519impl std::fmt::Debug for StaticTokenVerifier {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 f.debug_struct("StaticTokenVerifier")
522 .field("token_count", &self.tokens.len())
523 .field(
524 "allowed_scheme_count",
525 &self.allowed_schemes.as_ref().map_or(0, Vec::len),
526 )
527 .finish()
528 }
529}
530
531impl StaticTokenVerifier {
532 pub fn new<I, K>(tokens: I) -> McpResult<Self>
539 where
540 I: IntoIterator<Item = (K, AuthContext)>,
541 K: Into<String>,
542 {
543 let mut digests = HashMap::new();
544 for (token, context) in tokens {
545 if digests.len() >= MAX_STATIC_TOKEN_ENTRIES {
546 return Err(auth_error("Static token configuration is invalid"));
547 }
548 if context.subject.as_deref().is_none_or(str::is_empty)
549 || principal_fingerprint(Some(&context)).is_err()
550 {
551 return Err(auth_error("Static token configuration is invalid"));
552 }
553 let token = token.into();
554 if !AccessToken::is_valid_token68(&token) {
555 return Err(auth_error("Static token configuration is invalid"));
556 }
557 let digest = sha256_bounded(token.as_bytes(), MAX_ACCESS_TOKEN_BYTES)
558 .map_err(|_| auth_error("Static token configuration is invalid"))?;
559 digests
560 .try_reserve(1)
561 .map_err(|_| auth_error("Static token configuration is invalid"))?;
562 if digests.insert(digest, context).is_some() {
563 return Err(auth_error("Static token configuration is invalid"));
564 }
565 }
566 if digests.is_empty() {
567 return Err(auth_error("Static token configuration is invalid"));
568 }
569 Ok(Self {
570 tokens: digests,
571 allowed_schemes: None,
572 })
573 }
574
575 pub fn with_allowed_schemes<I, S>(mut self, schemes: I) -> McpResult<Self>
577 where
578 I: IntoIterator<Item = S>,
579 S: Into<String>,
580 {
581 let mut admitted = Vec::new();
582 for scheme in schemes {
583 if admitted.len() >= MAX_ALLOWED_AUTH_SCHEMES {
584 return Err(auth_error("Static auth scheme configuration is invalid"));
585 }
586 let scheme = scheme.into();
587 if !AccessToken::is_valid_http_scheme(&scheme) {
588 return Err(auth_error("Static auth scheme configuration is invalid"));
589 }
590 let normalized = scheme.to_ascii_lowercase();
591 if admitted.iter().any(|existing| existing == &normalized) {
592 return Err(auth_error("Static auth scheme configuration is invalid"));
593 }
594 admitted
595 .try_reserve(1)
596 .map_err(|_| auth_error("Static auth scheme configuration is invalid"))?;
597 admitted.push(normalized);
598 }
599 if admitted.is_empty() {
600 return Err(auth_error("Static auth scheme configuration is invalid"));
601 }
602 self.allowed_schemes = Some(admitted);
603 Ok(self)
604 }
605}
606
607impl TokenVerifier for StaticTokenVerifier {
608 fn verify(
609 &self,
610 _ctx: &McpContext,
611 _request: AuthRequest<'_>,
612 token: &AccessToken,
613 ) -> McpResult<AuthContext> {
614 if !AccessToken::is_valid_http_scheme(&token.scheme)
615 || !AccessToken::is_valid_token68(&token.token)
616 {
617 return Err(auth_error("Invalid access token"));
618 }
619 if let Some(allowed) = &self.allowed_schemes {
620 let normalized = token.scheme.to_ascii_lowercase();
621 if !allowed.contains(&normalized) {
622 return Err(auth_error("Unsupported auth scheme"));
623 }
624 }
625
626 let digest = sha256_bounded(token.token.as_bytes(), MAX_ACCESS_TOKEN_BYTES)
627 .map_err(|_| auth_error("Invalid access token"))?;
628 let Some(auth) = self.tokens.get(&digest) else {
629 return Err(auth_error("Invalid access token"));
630 };
631
632 Ok(auth.clone())
633 }
634}
635
636fn auth_error(message: impl Into<String>) -> McpError {
637 McpError::new(McpErrorCode::ResourceForbidden, message)
638}
639
640#[derive(Debug, Default, Clone, Copy)]
642pub struct AllowAllAuthProvider;
643
644impl AuthProvider for AllowAllAuthProvider {
645 fn authenticate(&self, _ctx: &McpContext, _request: AuthRequest<'_>) -> McpResult<AuthContext> {
646 Ok(AuthContext::anonymous())
647 }
648}
649
650#[cfg(test)]
655mod tests {
656 use super::*;
657 use asupersync::Cx;
658
659 fn ctx() -> McpContext {
660 McpContext::new(Cx::for_testing(), 1)
661 }
662
663 #[test]
664 fn access_token_parsers_separate_http_and_legacy_in_band_grammar() {
665 assert_eq!(
666 fastmcp_core::AccessToken::parse("Bearer abc"),
667 Some(fastmcp_core::AccessToken {
668 scheme: "Bearer".to_string(),
669 token: "abc".to_string(),
670 })
671 );
672 assert_eq!(
673 fastmcp_core::AccessToken::parse_legacy_in_band("abc"),
674 Some(fastmcp_core::AccessToken {
675 scheme: "Bearer".to_string(),
676 token: "abc".to_string(),
677 })
678 );
679 assert_eq!(
682 fastmcp_core::AccessToken::parse_legacy_in_band(" Bearer"),
683 Some(fastmcp_core::AccessToken {
684 scheme: "Bearer".to_string(),
685 token: "Bearer".to_string(),
686 })
687 );
688 assert_eq!(fastmcp_core::AccessToken::parse(""), None);
689 assert_eq!(fastmcp_core::AccessToken::parse(" "), None);
690 assert_eq!(fastmcp_core::AccessToken::parse("Bearer "), None);
691 assert_eq!(fastmcp_core::AccessToken::parse("abc"), None);
692 assert_eq!(fastmcp_core::AccessToken::parse("Bearer abc:def"), None);
693 }
694
695 #[test]
696 fn auth_request_extracts_access_token_from_common_locations() {
697 let req = AuthRequest {
699 method: "tools/call",
700 params: Some(&serde_json::Value::String("Bearer t1".to_string())),
701 transport_authorization: None,
702 request_id: 1,
703 };
704 assert_eq!(
705 req.access_token(),
706 Some(AccessToken {
707 scheme: "Bearer".to_string(),
708 token: "t1".to_string(),
709 })
710 );
711
712 let params = serde_json::json!({"authorization": "Bearer t2"});
714 let req = AuthRequest {
715 method: "tools/call",
716 params: Some(¶ms),
717 transport_authorization: None,
718 request_id: 1,
719 };
720 assert_eq!(
721 req.access_token(),
722 Some(AccessToken {
723 scheme: "Bearer".to_string(),
724 token: "t2".to_string(),
725 })
726 );
727
728 let params = serde_json::json!({"auth": {"scheme": "Bearer", "token": "t3"}});
730 let req = AuthRequest {
731 method: "tools/call",
732 params: Some(¶ms),
733 transport_authorization: None,
734 request_id: 1,
735 };
736 assert_eq!(
737 req.access_token(),
738 Some(AccessToken {
739 scheme: "Bearer".to_string(),
740 token: "t3".to_string(),
741 })
742 );
743
744 let params = serde_json::json!({"_meta": {"authorization": "Bearer t4"}});
746 let req = AuthRequest {
747 method: "tools/call",
748 params: Some(¶ms),
749 transport_authorization: None,
750 request_id: 1,
751 };
752 assert_eq!(
753 req.access_token(),
754 Some(AccessToken {
755 scheme: "Bearer".to_string(),
756 token: "t4".to_string(),
757 })
758 );
759
760 let params = serde_json::json!({"headers": {"Authorization": "Bearer t5"}});
762 let req = AuthRequest {
763 method: "tools/call",
764 params: Some(¶ms),
765 transport_authorization: None,
766 request_id: 1,
767 };
768 assert_eq!(
769 req.access_token(),
770 Some(AccessToken {
771 scheme: "Bearer".to_string(),
772 token: "t5".to_string(),
773 })
774 );
775 }
776
777 #[test]
778 fn native_and_in_band_authorization_are_rejected_as_ambiguous() {
779 let params = serde_json::json!({"authorization": "Bearer in-band"});
780 let request = AuthRequest {
781 method: "tools/call",
782 params: Some(¶ms),
783 transport_authorization: Some("Token native"),
784 request_id: 1,
785 };
786
787 assert!(request.access_token().is_none());
788 assert!(request.has_multiple_credential_sources());
789 }
790
791 #[test]
792 fn malformed_native_authorization_cannot_fall_back_in_band() {
793 let params = serde_json::json!({"authorization": "Bearer in-band"});
794 let request = AuthRequest {
795 method: "tools/call",
796 params: Some(¶ms),
797 transport_authorization: Some("Bearer "),
798 request_id: 1,
799 };
800
801 assert!(request.access_token().is_none());
802 }
803
804 #[test]
805 fn native_authorization_requires_http_token_scheme_and_token68_credential() {
806 for malformed in [
807 "Bearer",
808 "Basic",
809 "Bearer one two",
810 "Bearer\tcredential",
811 "Be(arer token",
812 "Bearer ab=c",
813 "Bearer credential,other",
814 " Bearer credential",
815 "Bearer credential ",
816 ] {
817 let request = AuthRequest {
818 method: "tools/call",
819 params: None,
820 transport_authorization: Some(malformed),
821 request_id: 1,
822 };
823 assert!(
824 request.access_token().is_none(),
825 "accepted malformed native authorization: {malformed:?}"
826 );
827 assert!(!request.credential_sources_are_admissible());
828 }
829
830 let request = AuthRequest {
831 method: "tools/call",
832 params: None,
833 transport_authorization: Some("Bearer abc_DEF-123+/=="),
834 request_id: 1,
835 };
836 let token = request.access_token().expect("valid native authorization");
837 assert_eq!(token.scheme, "Bearer");
838 assert_eq!(token.token, "abc_DEF-123+/==");
839 }
840
841 #[test]
842 fn normalized_http_authorization_still_uses_strict_native_grammar() {
843 use fastmcp_transport::http::HttpTransport;
844 use std::io::Cursor;
845
846 for (wire_value, accepted) in [
847 ("Bearer ", false),
848 ("Basic", false),
849 ("Bearer\tcredential", false),
850 ("Bearer one two", false),
851 ("Bearer abc_DEF-123==", true),
852 ] {
853 let wire = format!(
854 "POST /mcp HTTP/1.1\r\nHost: localhost\r\nAuthorization: {wire_value}\r\nContent-Length: 0\r\n\r\n"
855 );
856 let mut transport = HttpTransport::new(Cursor::new(wire.into_bytes()), Vec::new());
857 let http_request = transport.read_request().expect("parse HTTP request");
858 let auth_request = AuthRequest {
859 method: "tools/list",
860 params: None,
861 transport_authorization: http_request.authorization(),
862 request_id: 1,
863 };
864 assert_eq!(
865 auth_request.access_token().is_some(),
866 accepted,
867 "unexpected admission for normalized Authorization {wire_value:?}"
868 );
869 }
870 }
871
872 #[test]
873 fn auth_request_detects_multiple_credential_sources() {
874 let params = serde_json::json!({"_meta": {"accessToken": "Bearer in-band"}});
875 let multiple = AuthRequest {
876 method: "tools/call",
877 params: Some(¶ms),
878 transport_authorization: Some("Bearer native"),
879 request_id: 1,
880 };
881 assert!(multiple.has_multiple_credential_sources());
882
883 let application_params = serde_json::json!({"arguments": {"token": "application-data"}});
884 let native_only = AuthRequest {
885 method: "tools/call",
886 params: Some(&application_params),
887 transport_authorization: Some("Bearer native"),
888 request_id: 2,
889 };
890 assert!(!native_only.has_multiple_credential_sources());
891 }
892
893 #[test]
894 fn principal_fingerprint_separates_anonymous_from_authenticated_facts() {
895 let anonymous = principal_fingerprint(None).expect("anonymous fingerprint");
896 let admitted_anonymous =
897 principal_fingerprint(Some(&AuthContext::anonymous())).expect("provider fingerprint");
898 let alice =
899 principal_fingerprint(Some(&AuthContext::with_subject("alice"))).expect("fingerprint");
900 let alice_again =
901 principal_fingerprint(Some(&AuthContext::with_subject("alice"))).expect("fingerprint");
902
903 assert_ne!(anonymous, admitted_anonymous);
904 assert_ne!(anonymous, alice);
905 assert_ne!(admitted_anonymous, alice);
906 assert_eq!(alice, alice_again);
907
908 let mut alice_with_changed_authorization = AuthContext::with_subject("alice");
909 alice_with_changed_authorization.scopes = vec!["write".to_string(), "read".to_string()];
910 alice_with_changed_authorization.claims =
911 Some(serde_json::json!({"exp": 99, "policy_revision": 2}));
912 assert_eq!(
913 alice,
914 principal_fingerprint(Some(&alice_with_changed_authorization))
915 .expect("stable owner fingerprint")
916 );
917 assert_ne!(
918 alice,
919 principal_fingerprint(Some(&AuthContext::with_subject("bob")))
920 .expect("different owner fingerprint")
921 );
922 }
923
924 #[test]
925 fn principal_fingerprint_rejects_out_of_bounds_provider_facts() {
926 let oversized_subject = AuthContext::with_subject("s".repeat(MAX_AUTH_SUBJECT_BYTES + 1));
927 assert!(principal_fingerprint(Some(&oversized_subject)).is_err());
928
929 assert!(principal_fingerprint(Some(&AuthContext::with_subject(""))).is_err());
930
931 let mut empty_scope = AuthContext::anonymous();
932 empty_scope.scopes = vec![String::new()];
933 assert!(principal_fingerprint(Some(&empty_scope)).is_err());
934
935 let mut too_many_scopes = AuthContext::anonymous();
936 too_many_scopes.scopes = vec!["scope".to_string(); MAX_AUTH_SCOPES + 1];
937 assert!(principal_fingerprint(Some(&too_many_scopes)).is_err());
938
939 let mut oversized_claim = AuthContext::anonymous();
940 oversized_claim.claims = Some(serde_json::Value::String(
941 "c".repeat(MAX_AUTH_CLAIM_STRING_BYTES + 1),
942 ));
943 assert!(principal_fingerprint(Some(&oversized_claim)).is_err());
944
945 let mut oversized_aggregate = AuthContext::with_subject("owner");
946 oversized_aggregate.claims = Some(serde_json::Value::Array(vec![
947 serde_json::Value::String(
948 "c".repeat(MAX_AUTH_CLAIM_STRING_BYTES)
949 );
950 5
951 ]));
952 assert!(claims_shape_is_bounded(
953 oversized_aggregate.claims.as_ref().unwrap()
954 ));
955 assert!(principal_fingerprint(Some(&oversized_aggregate)).is_err());
956 }
957
958 #[test]
959 fn token_auth_provider_errors_on_missing_token_and_allows_override() {
960 #[derive(Debug)]
961 struct AcceptAll;
962 impl TokenVerifier for AcceptAll {
963 fn verify(
964 &self,
965 _ctx: &McpContext,
966 _request: AuthRequest<'_>,
967 _token: &AccessToken,
968 ) -> McpResult<AuthContext> {
969 Ok(AuthContext::with_subject("ok"))
970 }
971 }
972
973 let provider = TokenAuthProvider::new(AcceptAll);
974 let req = AuthRequest {
975 method: "tools/call",
976 params: None,
977 transport_authorization: None,
978 request_id: 1,
979 };
980 let err = provider.authenticate(&ctx(), req).unwrap_err();
981 assert_eq!(err.code, McpErrorCode::ResourceForbidden);
982 assert!(err.message.contains("Missing access token"));
983
984 let provider =
985 TokenAuthProvider::new(AcceptAll).with_missing_token_error(auth_error("no token"));
986 let req = AuthRequest {
987 method: "tools/call",
988 params: None,
989 transport_authorization: None,
990 request_id: 1,
991 };
992 let err = provider.authenticate(&ctx(), req).unwrap_err();
993 assert!(err.message.contains("no token"));
994 }
995
996 #[test]
997 fn static_token_verifier_enforces_scheme_without_exposing_token() {
998 let mut base = AuthContext::with_subject("user123");
999 base.scopes = vec!["read".to_string()];
1000
1001 let verifier = StaticTokenVerifier::new([("value-1", base.clone())])
1002 .expect("valid verifier configuration")
1003 .with_allowed_schemes(["Bearer"])
1004 .expect("valid scheme configuration");
1005 let req = AuthRequest {
1006 method: "tools/call",
1007 params: None,
1008 transport_authorization: None,
1009 request_id: 1,
1010 };
1011
1012 let err = verifier
1014 .verify(
1015 &ctx(),
1016 req,
1017 &AccessToken {
1018 scheme: "Basic".to_string(),
1019 token: "value-1".to_string(),
1020 },
1021 )
1022 .unwrap_err();
1023 assert!(err.message.contains("Unsupported auth scheme"));
1024
1025 let auth = verifier
1027 .verify(
1028 &ctx(),
1029 req,
1030 &AccessToken {
1031 scheme: "bearer".to_string(),
1032 token: "value-1".to_string(),
1033 },
1034 )
1035 .unwrap();
1036 assert_eq!(auth.subject, Some("user123".to_string()));
1037 assert_eq!(auth.scopes, vec!["read".to_string()]);
1038 let serialized = serde_json::to_string(&auth).expect("serialize verified auth facts");
1039 assert!(!serialized.contains("value-1"));
1040 }
1041
1042 #[test]
1043 fn allow_all_provider_returns_anonymous_context() {
1044 let provider = AllowAllAuthProvider;
1045 let req = AuthRequest {
1046 method: "tools/call",
1047 params: None,
1048 transport_authorization: None,
1049 request_id: 1,
1050 };
1051 let auth = provider.authenticate(&ctx(), req).unwrap();
1052 assert_eq!(auth.subject, None);
1053 assert!(auth.scopes.is_empty());
1054 }
1055
1056 #[test]
1057 fn access_token_from_none_params() {
1058 let req = AuthRequest {
1059 method: "tools/call",
1060 params: None,
1061 transport_authorization: None,
1062 request_id: 1,
1063 };
1064 assert!(req.access_token().is_none());
1065 }
1066
1067 #[test]
1068 fn access_token_from_array_params() {
1069 let params = serde_json::json!([1, 2, 3]);
1070 let req = AuthRequest {
1071 method: "tools/call",
1072 params: Some(¶ms),
1073 transport_authorization: None,
1074 request_id: 1,
1075 };
1076 assert!(req.access_token().is_none());
1077 }
1078
1079 #[test]
1080 fn access_token_from_number_params() {
1081 let params = serde_json::json!(42);
1082 let req = AuthRequest {
1083 method: "tools/call",
1084 params: Some(¶ms),
1085 transport_authorization: None,
1086 request_id: 1,
1087 };
1088 assert!(req.access_token().is_none());
1089 }
1090
1091 #[test]
1092 fn access_token_from_object_with_token_field() {
1093 let params = serde_json::json!({"token": "Bearer my-secret"});
1094 let req = AuthRequest {
1095 method: "tools/call",
1096 params: Some(¶ms),
1097 transport_authorization: None,
1098 request_id: 1,
1099 };
1100 let token = req.access_token().expect("should extract token");
1101 assert_eq!(token.scheme, "Bearer");
1102 assert_eq!(token.token, "my-secret");
1103 }
1104
1105 #[test]
1106 fn access_token_from_object_with_access_token_field() {
1107 let params = serde_json::json!({"access_token": "abc123"});
1108 let req = AuthRequest {
1109 method: "tools/call",
1110 params: Some(¶ms),
1111 transport_authorization: None,
1112 request_id: 1,
1113 };
1114 let token = req.access_token().expect("should extract");
1115 assert_eq!(token.scheme, "Bearer");
1117 assert_eq!(token.token, "abc123");
1118 }
1119
1120 #[test]
1121 fn access_token_from_camel_case_field() {
1122 let params = serde_json::json!({"accessToken": "Bearer xyz"});
1123 let req = AuthRequest {
1124 method: "tools/call",
1125 params: Some(¶ms),
1126 transport_authorization: None,
1127 request_id: 1,
1128 };
1129 let token = req.access_token().expect("should extract");
1130 assert_eq!(token.token, "xyz");
1131 }
1132
1133 #[test]
1134 fn access_token_from_nested_scheme_token_object_rejects_empty_scheme() {
1135 let params = serde_json::json!({"auth": {"scheme": "", "token": "abc"}});
1136 let req = AuthRequest {
1137 method: "tools/call",
1138 params: Some(¶ms),
1139 transport_authorization: None,
1140 request_id: 1,
1141 };
1142 assert_eq!(req.access_token(), None);
1143 }
1144
1145 #[test]
1146 fn access_token_from_nested_scheme_token_object_with_whitespace_token() {
1147 let params = serde_json::json!({"authorization": " "});
1150 let req = AuthRequest {
1151 method: "tools/call",
1152 params: Some(¶ms),
1153 transport_authorization: None,
1154 request_id: 1,
1155 };
1156 assert!(req.access_token().is_none());
1157 }
1158
1159 #[test]
1160 fn static_verifier_rejects_unknown_token() {
1161 let verifier =
1162 StaticTokenVerifier::new([("valid-token", AuthContext::with_subject("owner"))])
1163 .expect("valid verifier configuration");
1164 let req = AuthRequest {
1165 method: "tools/call",
1166 params: None,
1167 transport_authorization: None,
1168 request_id: 1,
1169 };
1170 let err = verifier
1171 .verify(
1172 &ctx(),
1173 req,
1174 &AccessToken {
1175 scheme: "Bearer".to_string(),
1176 token: "wrong-token".to_string(),
1177 },
1178 )
1179 .unwrap_err();
1180 assert_eq!(err.code, McpErrorCode::ResourceForbidden);
1181 assert!(err.message.contains("Invalid access token"));
1182 }
1183
1184 #[test]
1185 fn static_verifier_no_scheme_restriction_allows_any() {
1186 let verifier = StaticTokenVerifier::new([("tok", AuthContext::with_subject("alice"))])
1187 .expect("valid verifier configuration");
1188 let req = AuthRequest {
1189 method: "tools/call",
1190 params: None,
1191 transport_authorization: None,
1192 request_id: 1,
1193 };
1194 let auth = verifier
1195 .verify(
1196 &ctx(),
1197 req,
1198 &AccessToken {
1199 scheme: "CustomScheme".to_string(),
1200 token: "tok".to_string(),
1201 },
1202 )
1203 .unwrap();
1204 assert_eq!(auth.subject, Some("alice".to_string()));
1205 }
1206
1207 #[test]
1208 fn token_auth_provider_succeeds_with_valid_token() {
1209 let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("bob"))])
1210 .expect("valid verifier configuration");
1211 let provider = TokenAuthProvider::new(verifier);
1212 let params = serde_json::json!({"authorization": "Bearer secret"});
1213 let req = AuthRequest {
1214 method: "tools/call",
1215 params: Some(¶ms),
1216 transport_authorization: None,
1217 request_id: 1,
1218 };
1219 let auth = provider.authenticate(&ctx(), req).unwrap();
1220 assert_eq!(auth.subject, Some("bob".to_string()));
1221 }
1222
1223 #[test]
1224 fn token_auth_provider_fails_with_wrong_token() {
1225 let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("bob"))])
1226 .expect("valid verifier configuration");
1227 let provider = TokenAuthProvider::new(verifier);
1228 let params = serde_json::json!({"authorization": "Bearer wrong"});
1229 let req = AuthRequest {
1230 method: "tools/call",
1231 params: Some(¶ms),
1232 transport_authorization: None,
1233 request_id: 1,
1234 };
1235 let err = provider.authenticate(&ctx(), req).unwrap_err();
1236 assert_eq!(err.code, McpErrorCode::ResourceForbidden);
1237 }
1238
1239 #[test]
1240 fn auth_request_debug() {
1241 let params = serde_json::json!({"AUTH_PARAMS_DEBUG_CANARY": "AUTH_VALUE_DEBUG_CANARY"});
1242 let req = AuthRequest {
1243 method: "AUTH_METHOD_DEBUG_CANARY",
1244 params: Some(¶ms),
1245 transport_authorization: None,
1246 request_id: 42,
1247 };
1248 let debug = format!("{req:?}");
1249 assert!(debug.contains("method_bytes"));
1250 assert!(debug.contains("42"));
1251 assert!(!debug.contains("AUTH_METHOD_DEBUG_CANARY"));
1252 assert!(!debug.contains("AUTH_PARAMS_DEBUG_CANARY"));
1253 assert!(!debug.contains("AUTH_VALUE_DEBUG_CANARY"));
1254 }
1255
1256 #[test]
1257 fn credential_stripping_removes_only_recognized_locations() {
1258 let mut params = Some(serde_json::json!({
1259 "authorization": "Bearer top-secret",
1260 "auth": {"token": "nested-top-secret"},
1261 "_meta": {
1262 "accessToken": "meta-secret",
1263 "trace": "keep-meta"
1264 },
1265 "headers": {
1266 "Authorization": "Bearer header-secret",
1267 "content-type": "application/json"
1268 },
1269 "arguments": {
1270 "token": "application-data",
1271 "nested": {"authorization": "application-data-too"}
1272 },
1273 "name": "tool-name"
1274 }));
1275
1276 strip_recognized_access_credentials(&mut params);
1277
1278 assert_eq!(
1279 params,
1280 Some(serde_json::json!({
1281 "_meta": {"trace": "keep-meta"},
1282 "headers": {"content-type": "application/json"},
1283 "arguments": {
1284 "token": "application-data",
1285 "nested": {"authorization": "application-data-too"}
1286 },
1287 "name": "tool-name"
1288 }))
1289 );
1290 }
1291
1292 #[test]
1293 fn credential_stripping_removes_legacy_bare_string_payload() {
1294 let mut params = Some(serde_json::json!("Bearer secret"));
1295 strip_recognized_access_credentials(&mut params);
1296 assert_eq!(params, None);
1297 }
1298
1299 #[test]
1300 fn auth_request_clone_copy() {
1301 let req = AuthRequest {
1302 method: "test",
1303 params: None,
1304 transport_authorization: None,
1305 request_id: 1,
1306 };
1307 let req2 = req; assert_eq!(req.method, req2.method);
1309 assert_eq!(req.request_id, req2.request_id);
1310 }
1311
1312 #[test]
1313 fn access_token_from_headers_nested_object() {
1314 let params = serde_json::json!({
1316 "headers": {
1317 "Authorization": {"scheme": "Bearer", "token": "hdr-tok"}
1318 }
1319 });
1320 let req = AuthRequest {
1321 method: "tools/call",
1322 params: Some(¶ms),
1323 transport_authorization: None,
1324 request_id: 1,
1325 };
1326 let token = req.access_token().expect("should extract from headers");
1327 assert_eq!(token.scheme, "Bearer");
1328 assert_eq!(token.token, "hdr-tok");
1329 }
1330
1331 #[test]
1332 fn access_token_from_empty_object() {
1333 let params = serde_json::json!({});
1334 let req = AuthRequest {
1335 method: "tools/call",
1336 params: Some(¶ms),
1337 transport_authorization: None,
1338 request_id: 1,
1339 };
1340 assert!(req.access_token().is_none());
1341 }
1342
1343 #[test]
1346 fn allow_all_provider_debug() {
1347 let provider = AllowAllAuthProvider;
1348 let debug = format!("{provider:?}");
1349 assert!(debug.contains("AllowAllAuthProvider"));
1350 }
1351
1352 #[test]
1353 fn allow_all_provider_default() {
1354 let _ = AllowAllAuthProvider;
1355 }
1356
1357 #[test]
1358 fn allow_all_provider_clone_copy() {
1359 let provider = AllowAllAuthProvider;
1360 let cloned = provider.clone();
1361 let copied = provider; let _ = cloned
1363 .authenticate(
1364 &ctx(),
1365 AuthRequest {
1366 method: "test",
1367 params: None,
1368 transport_authorization: None,
1369 request_id: 1,
1370 },
1371 )
1372 .unwrap();
1373 let _ = copied;
1374 }
1375
1376 #[test]
1379 fn token_auth_provider_clone() {
1380 let verifier =
1381 StaticTokenVerifier::new([("tok", AuthContext::with_subject("clone-owner"))])
1382 .expect("valid verifier configuration");
1383 let provider = TokenAuthProvider::new(verifier);
1384 let cloned = provider.clone();
1385 let params = serde_json::json!({"authorization": "Bearer tok"});
1386 let req = AuthRequest {
1387 method: "tools/call",
1388 params: Some(¶ms),
1389 transport_authorization: None,
1390 request_id: 1,
1391 };
1392 let auth = cloned.authenticate(&ctx(), req).unwrap();
1393 assert_eq!(auth.subject.as_deref(), Some("clone-owner"));
1394 }
1395
1396 #[test]
1397 fn token_auth_provider_with_custom_error_and_valid_token() {
1398 let verifier = StaticTokenVerifier::new([("valid", AuthContext::with_subject("user"))])
1399 .expect("valid verifier configuration");
1400 let provider =
1401 TokenAuthProvider::new(verifier).with_missing_token_error(auth_error("custom missing"));
1402 let params = serde_json::json!({"authorization": "Bearer valid"});
1403 let req = AuthRequest {
1404 method: "tools/call",
1405 params: Some(¶ms),
1406 transport_authorization: None,
1407 request_id: 1,
1408 };
1409 let auth = provider.authenticate(&ctx(), req).unwrap();
1410 assert_eq!(auth.subject, Some("user".to_string()));
1411 }
1412
1413 #[test]
1416 fn static_verifier_debug_redacts_configured_tokens() {
1417 let canary = "STATIC_TOKEN_DEBUG_CANARY";
1418 let verifier = StaticTokenVerifier::new([(canary, AuthContext::with_subject("owner"))])
1419 .expect("valid verifier configuration");
1420 let debug = format!("{verifier:?}");
1421 assert!(debug.contains("StaticTokenVerifier"));
1422 assert!(debug.contains("token_count"));
1423 assert!(!debug.contains(canary));
1424 }
1425
1426 #[test]
1427 fn static_verifier_fails_closed_for_oversized_configured_or_presented_tokens() {
1428 let oversized = "x".repeat(MAX_ACCESS_TOKEN_BYTES + 1);
1429 let configured = StaticTokenVerifier::new([(
1430 oversized.clone(),
1431 AuthContext::with_subject("must-not-load"),
1432 )]);
1433 let config_error = configured.expect_err("oversized configured token must fail closed");
1434 assert_eq!(config_error.code, McpErrorCode::ResourceForbidden);
1435 assert!(!config_error.message.contains(&oversized));
1436
1437 let verifier = StaticTokenVerifier::new([("valid", AuthContext::with_subject("owner"))])
1438 .expect("valid verifier configuration");
1439 let request = AuthRequest {
1440 method: "test",
1441 params: None,
1442 transport_authorization: None,
1443 request_id: 1,
1444 };
1445 let error = verifier
1446 .verify(
1447 &ctx(),
1448 request,
1449 &AccessToken {
1450 scheme: "Bearer".to_string(),
1451 token: oversized.clone(),
1452 },
1453 )
1454 .expect_err("oversized presented token must fail closed");
1455 assert_eq!(error.code, McpErrorCode::ResourceForbidden);
1456 assert!(!error.message.contains(&oversized));
1457 }
1458
1459 #[test]
1460 fn static_verifier_multiple_tokens() {
1461 let verifier = StaticTokenVerifier::new([
1462 ("alpha", AuthContext::with_subject("alice")),
1463 ("beta", AuthContext::with_subject("bob")),
1464 ])
1465 .expect("valid verifier configuration");
1466 let req = AuthRequest {
1467 method: "test",
1468 params: None,
1469 transport_authorization: None,
1470 request_id: 1,
1471 };
1472 let a = verifier
1473 .verify(
1474 &ctx(),
1475 req,
1476 &AccessToken {
1477 scheme: "Bearer".to_string(),
1478 token: "alpha".to_string(),
1479 },
1480 )
1481 .unwrap();
1482 assert_eq!(a.subject, Some("alice".to_string()));
1483 let b = verifier
1484 .verify(
1485 &ctx(),
1486 req,
1487 &AccessToken {
1488 scheme: "Bearer".to_string(),
1489 token: "beta".to_string(),
1490 },
1491 )
1492 .unwrap();
1493 assert_eq!(b.subject, Some("bob".to_string()));
1494 }
1495
1496 #[test]
1497 fn static_verifier_multiple_allowed_schemes() {
1498 let verifier = StaticTokenVerifier::new([("tok", AuthContext::with_subject("owner"))])
1499 .expect("valid verifier configuration")
1500 .with_allowed_schemes(["Bearer", "Token"])
1501 .expect("valid scheme configuration");
1502 let req = AuthRequest {
1503 method: "test",
1504 params: None,
1505 transport_authorization: None,
1506 request_id: 1,
1507 };
1508 assert!(
1510 verifier
1511 .verify(
1512 &ctx(),
1513 req,
1514 &AccessToken {
1515 scheme: "Bearer".to_string(),
1516 token: "tok".to_string(),
1517 },
1518 )
1519 .is_ok()
1520 );
1521 assert!(
1523 verifier
1524 .verify(
1525 &ctx(),
1526 req,
1527 &AccessToken {
1528 scheme: "Token".to_string(),
1529 token: "tok".to_string(),
1530 },
1531 )
1532 .is_ok()
1533 );
1534 assert!(
1536 verifier
1537 .verify(
1538 &ctx(),
1539 req,
1540 &AccessToken {
1541 scheme: "Basic".to_string(),
1542 token: "tok".to_string(),
1543 },
1544 )
1545 .is_err()
1546 );
1547 }
1548
1549 #[test]
1552 fn access_token_from_bool_value_returns_none() {
1553 let params = serde_json::json!(true);
1554 let req = AuthRequest {
1555 method: "test",
1556 params: Some(¶ms),
1557 transport_authorization: None,
1558 request_id: 1,
1559 };
1560 assert!(req.access_token().is_none());
1561 }
1562
1563 #[test]
1564 fn access_token_from_null_params() {
1565 let params = serde_json::json!(null);
1566 let req = AuthRequest {
1567 method: "test",
1568 params: Some(¶ms),
1569 transport_authorization: None,
1570 request_id: 1,
1571 };
1572 assert!(req.access_token().is_none());
1573 }
1574
1575 #[test]
1576 fn access_token_from_nested_object_with_inner_authorization_string() {
1577 let params = serde_json::json!({
1579 "auth": {
1580 "authorization": "Bearer inner-tok"
1581 }
1582 });
1583 let req = AuthRequest {
1584 method: "test",
1585 params: Some(¶ms),
1586 transport_authorization: None,
1587 request_id: 1,
1588 };
1589 let token = req.access_token().expect("should extract from nested auth");
1590 assert_eq!(token.token, "inner-tok");
1591 }
1592
1593 #[test]
1596 fn access_token_meta_fallback_when_top_level_empty() {
1597 let params = serde_json::json!({
1598 "other_field": 123,
1599 "_meta": {"authorization": "Bearer meta-tok"}
1600 });
1601 let req = AuthRequest {
1602 method: "test",
1603 params: Some(¶ms),
1604 transport_authorization: None,
1605 request_id: 1,
1606 };
1607 let token = req.access_token().expect("should fallback to _meta");
1608 assert_eq!(token.token, "meta-tok");
1609 }
1610
1611 #[test]
1612 fn access_token_headers_fallback_when_top_and_meta_empty() {
1613 let params = serde_json::json!({
1614 "other": "value",
1615 "_meta": {"other": "value"},
1616 "headers": {"authorization": "Bearer hdr-tok"}
1617 });
1618 let req = AuthRequest {
1619 method: "test",
1620 params: Some(¶ms),
1621 transport_authorization: None,
1622 request_id: 1,
1623 };
1624 let token = req.access_token().expect("should fallback to headers");
1625 assert_eq!(token.token, "hdr-tok");
1626 }
1627
1628 #[test]
1629 fn access_token_rejects_multiple_in_band_locations() {
1630 let params = serde_json::json!({
1631 "authorization": "Bearer top-tok",
1632 "_meta": {"authorization": "Bearer meta-tok"}
1633 });
1634 let req = AuthRequest {
1635 method: "test",
1636 params: Some(¶ms),
1637 transport_authorization: None,
1638 request_id: 1,
1639 };
1640 assert!(req.access_token().is_none());
1641 assert!(req.has_multiple_credential_sources());
1642 }
1643
1644 #[test]
1645 fn access_token_reports_multiple_sources_inside_nested_credential_object() {
1646 for params in [
1647 serde_json::json!({
1648 "auth": {
1649 "authorization": "Bearer first",
1650 "access_token": "Bearer second"
1651 }
1652 }),
1653 serde_json::json!({
1654 "auth": {
1655 "scheme": "Bearer",
1656 "token": "first",
1657 "authorization": "Bearer second"
1658 }
1659 }),
1660 ] {
1661 let req = AuthRequest {
1662 method: "test",
1663 params: Some(¶ms),
1664 transport_authorization: None,
1665 request_id: 1,
1666 };
1667 assert!(req.access_token().is_none());
1668 assert!(req.has_multiple_credential_sources());
1669 assert!(!req.credential_sources_are_admissible());
1670 }
1671 }
1672
1673 #[test]
1676 fn auth_error_creates_resource_forbidden() {
1677 let err = auth_error("denied");
1678 assert_eq!(err.code, McpErrorCode::ResourceForbidden);
1679 assert!(err.message.contains("denied"));
1680 }
1681
1682 #[test]
1685 fn access_token_from_object_without_any_known_key() {
1686 let params = serde_json::json!({"unknown_key": "Bearer tok"});
1687 let req = AuthRequest {
1688 method: "test",
1689 params: Some(¶ms),
1690 transport_authorization: None,
1691 request_id: 1,
1692 };
1693 assert!(req.access_token().is_none());
1694 }
1695
1696 #[test]
1697 fn access_token_from_scheme_token_with_whitespace_only_scheme() {
1698 let params = serde_json::json!({"auth": {"scheme": " ", "token": "abc"}});
1699 let req = AuthRequest {
1700 method: "test",
1701 params: Some(¶ms),
1702 transport_authorization: None,
1703 request_id: 1,
1704 };
1705 assert!(req.access_token().is_none());
1706 assert!(!req.credential_sources_are_admissible());
1707 }
1708
1709 #[test]
1712 fn access_token_meta_non_object_falls_through_to_headers() {
1713 let params = serde_json::json!({
1714 "_meta": 42,
1715 "headers": {"authorization": "Bearer hdr"}
1716 });
1717 let req = AuthRequest {
1718 method: "test",
1719 params: Some(¶ms),
1720 transport_authorization: None,
1721 request_id: 1,
1722 };
1723 let token = req.access_token().expect("should skip non-object _meta");
1724 assert_eq!(token.token, "hdr");
1725 }
1726
1727 #[test]
1728 fn access_token_headers_non_object_returns_none() {
1729 let params = serde_json::json!({
1730 "_meta": {"other": true},
1731 "headers": "not-an-object"
1732 });
1733 let req = AuthRequest {
1734 method: "test",
1735 params: Some(¶ms),
1736 transport_authorization: None,
1737 request_id: 1,
1738 };
1739 assert!(req.access_token().is_none());
1740 }
1741
1742 #[test]
1745 fn access_token_map_field_with_numeric_value_returns_none() {
1746 let params = serde_json::json!({"authorization": 12345});
1747 let req = AuthRequest {
1748 method: "test",
1749 params: Some(¶ms),
1750 transport_authorization: None,
1751 request_id: 1,
1752 };
1753 assert!(req.access_token().is_none());
1754 }
1755
1756 #[test]
1757 fn access_token_map_field_with_bool_value_returns_none() {
1758 let params = serde_json::json!({"token": true});
1759 let req = AuthRequest {
1760 method: "test",
1761 params: Some(¶ms),
1762 transport_authorization: None,
1763 request_id: 1,
1764 };
1765 assert!(req.access_token().is_none());
1766 }
1767
1768 #[test]
1769 fn access_token_map_field_with_array_value_returns_none() {
1770 let params = serde_json::json!({"authorization": ["Bearer", "tok"]});
1771 let req = AuthRequest {
1772 method: "test",
1773 params: Some(¶ms),
1774 transport_authorization: None,
1775 request_id: 1,
1776 };
1777 assert!(req.access_token().is_none());
1778 }
1779
1780 #[test]
1783 fn access_token_nested_object_with_access_token_key() {
1784 let params = serde_json::json!({
1785 "auth": {
1786 "accessToken": "Bearer nested-at"
1787 }
1788 });
1789 let req = AuthRequest {
1790 method: "test",
1791 params: Some(¶ms),
1792 transport_authorization: None,
1793 request_id: 1,
1794 };
1795 let token = req
1796 .access_token()
1797 .expect("should extract from nested accessToken");
1798 assert_eq!(token.token, "nested-at");
1799 }
1800
1801 #[test]
1804 fn static_verifier_rejects_empty_allowed_scheme_configuration() {
1805 let error = StaticTokenVerifier::new([("tok", AuthContext::with_subject("owner"))])
1806 .expect("valid verifier configuration")
1807 .with_allowed_schemes(Vec::<String>::new())
1808 .expect_err("an empty scheme policy is a configuration error");
1809 assert_eq!(error.code, McpErrorCode::ResourceForbidden);
1810 }
1811
1812 #[test]
1813 fn static_verifier_rejects_malformed_and_duplicate_configuration() {
1814 for context in [AuthContext::anonymous(), AuthContext::with_subject("")] {
1815 let error = StaticTokenVerifier::new([("token", context)])
1816 .expect_err("a configured credential must have a nonempty owner subject");
1817 assert_eq!(error.code, McpErrorCode::ResourceForbidden);
1818 assert_eq!(error.message, "Static token configuration is invalid");
1819 }
1820
1821 let oversized_subject = AuthContext::with_subject("x".repeat(MAX_AUTH_SUBJECT_BYTES + 1));
1822 let error = StaticTokenVerifier::new([("token", oversized_subject)])
1823 .expect_err("inadmissible authentication facts must fail at configuration time");
1824 assert_eq!(error.code, McpErrorCode::ResourceForbidden);
1825 assert_eq!(error.message, "Static token configuration is invalid");
1826
1827 for token in ["", " ", " leading", "trailing ", "two words", "bad:token"] {
1828 assert!(
1829 StaticTokenVerifier::new([(token, AuthContext::with_subject("owner"))]).is_err(),
1830 "accepted malformed static token {token:?}"
1831 );
1832 }
1833
1834 assert!(
1835 StaticTokenVerifier::new([
1836 ("duplicate", AuthContext::with_subject("first")),
1837 ("duplicate", AuthContext::with_subject("second")),
1838 ])
1839 .is_err()
1840 );
1841
1842 let verifier = StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
1843 .expect("valid verifier configuration");
1844 assert!(verifier.with_allowed_schemes(["Bearer", "bearer"]).is_err());
1845 let verifier = StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
1846 .expect("valid verifier configuration");
1847 assert!(verifier.with_allowed_schemes(["Bad Scheme"]).is_err());
1848 }
1849
1850 #[test]
1851 fn static_verifier_enforces_exact_entry_and_scheme_boundaries() {
1852 let empty = StaticTokenVerifier::new(Vec::<(String, AuthContext)>::new())
1853 .expect_err("an empty static-token map is not an authentication policy");
1854 assert_eq!(empty.code, McpErrorCode::ResourceForbidden);
1855
1856 let maximum_entries = (0..MAX_STATIC_TOKEN_ENTRIES)
1857 .map(|index| {
1858 (
1859 format!("token-{index}"),
1860 AuthContext::with_subject(format!("owner-{index}")),
1861 )
1862 })
1863 .collect::<Vec<_>>();
1864 assert_eq!(
1865 StaticTokenVerifier::new(maximum_entries)
1866 .expect("the documented entry maximum is admissible")
1867 .tokens
1868 .len(),
1869 MAX_STATIC_TOKEN_ENTRIES
1870 );
1871
1872 let excessive_entries = (0..=MAX_STATIC_TOKEN_ENTRIES)
1873 .map(|index| {
1874 (
1875 format!("token-{index}"),
1876 AuthContext::with_subject(format!("owner-{index}")),
1877 )
1878 })
1879 .collect::<Vec<_>>();
1880 assert!(StaticTokenVerifier::new(excessive_entries).is_err());
1881
1882 let maximum_schemes = (0..MAX_ALLOWED_AUTH_SCHEMES)
1883 .map(|index| format!("Scheme{index}"))
1884 .collect::<Vec<_>>();
1885 StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
1886 .expect("valid verifier configuration")
1887 .with_allowed_schemes(maximum_schemes)
1888 .expect("the documented scheme maximum is admissible");
1889
1890 let excessive_schemes = (0..=MAX_ALLOWED_AUTH_SCHEMES)
1891 .map(|index| format!("Scheme{index}"))
1892 .collect::<Vec<_>>();
1893 assert!(
1894 StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
1895 .expect("valid verifier configuration")
1896 .with_allowed_schemes(excessive_schemes)
1897 .is_err()
1898 );
1899 }
1900
1901 #[test]
1904 fn token_auth_provider_with_scheme_restriction() {
1905 let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("user"))])
1906 .expect("valid verifier configuration")
1907 .with_allowed_schemes(["Bearer"])
1908 .expect("valid scheme configuration");
1909 let provider = TokenAuthProvider::new(verifier);
1910
1911 let params = serde_json::json!({"authorization": "Basic secret"});
1913 let req = AuthRequest {
1914 method: "test",
1915 params: Some(¶ms),
1916 transport_authorization: None,
1917 request_id: 1,
1918 };
1919 let err = provider.authenticate(&ctx(), req).unwrap_err();
1920 assert!(err.message.contains("Unsupported"));
1921
1922 let params = serde_json::json!({"authorization": "Bearer secret"});
1924 let req = AuthRequest {
1925 method: "test",
1926 params: Some(¶ms),
1927 transport_authorization: None,
1928 request_id: 1,
1929 };
1930 let auth = provider.authenticate(&ctx(), req).unwrap();
1931 assert_eq!(auth.subject, Some("user".to_string()));
1932 }
1933
1934 #[test]
1937 fn auth_request_exposes_all_fields() {
1938 let params = serde_json::json!({"key": "val"});
1939 let req = AuthRequest {
1940 method: "prompts/get",
1941 params: Some(¶ms),
1942 transport_authorization: None,
1943 request_id: 99,
1944 };
1945 assert_eq!(req.method, "prompts/get");
1946 assert_eq!(req.request_id, 99);
1947 assert!(req.params.is_some());
1948 }
1949}