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 request
4//! payload. Successful authentication is committed exactly once to the
5//! request-local [`McpContext`]; credentials and identity are never persisted
6//! in session state.
7
8use 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        // Allocate separately so failure leaves the previous canonical bytes
65        // intact and allocator over-allocation cannot silently exceed the
66        // admission limit.
67        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/// Authentication request view used by providers.
225#[derive(Clone, Copy)]
226pub struct AuthRequest<'a> {
227    /// JSON-RPC method name.
228    pub method: &'a str,
229    /// Raw params payload (if present).
230    ///
231    /// Inspecting credentials in JSON-RPC params is a legacy,
232    /// transport-neutral fallback. Transport integrations should authenticate
233    /// from their native authorization metadata instead. Any credential field
234    /// recognized here is removed before extension middleware and handlers
235    /// receive the request.
236    pub params: Option<&'a serde_json::Value>,
237    /// Transport-private `Authorization` field, when the transport has one.
238    ///
239    /// This value is never inserted into JSON-RPC params or exposed to
240    /// middleware and handlers. When present it must satisfy the strict native
241    /// header grammar, and no legacy in-band credential may coexist with it.
242    pub transport_authorization: Option<&'a str>,
243    /// Internal request ID (u64) used for tracing.
244    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    /// Returns the one strictly admitted native or legacy access token.
263    ///
264    /// Malformed or ambiguous credential sources return `None`; the server
265    /// rejects those cases before invoking an authentication provider.
266    #[must_use]
267    pub fn access_token(&self) -> Option<AccessToken> {
268        admitted_access_token(*self).ok().flatten()
269    }
270
271    /// Returns true when more than one recognized credential source is
272    /// present, including multiple legacy in-band locations.
273    #[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
416/// Removes only the JSON locations treated as in-band credentials by
417/// [`AuthRequest::access_token`].
418///
419/// This intentionally does not recurse into tool arguments or arbitrary
420/// application objects. Fields that authentication recognizes must not remain
421/// visible to extension middleware or handlers after the provider has used
422/// them, while unrelated protocol and application data is preserved.
423pub(crate) fn strip_recognized_access_credentials(params: &mut Option<serde_json::Value>) {
424    // A bare string is supported only as a legacy token-only payload, so
425    // there is no non-credential parameter value to retain.
426    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
451/// Authentication provider interface.
452///
453/// Implementations decide whether a request is allowed and may return
454/// an [`AuthContext`] describing the authenticated subject.
455pub trait AuthProvider: Send + Sync {
456    /// Authenticate an incoming request.
457    ///
458    /// Return `Ok(AuthContext)` to allow, or an `Err(McpError)` to deny. When
459    /// admitting a credential, `AuthContext::subject` must be a nonempty,
460    /// stable, provider-scoped owner identifier. Scopes and claims are
461    /// authorization facts and are deliberately not session-owner identity.
462    /// Provider error messages and data are treated as private diagnostics and
463    /// are replaced at the framework boundary before middleware or peers see
464    /// them.
465    fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext>;
466}
467
468/// Token verifier interface used by token-based auth providers.
469pub trait TokenVerifier: Send + Sync {
470    /// Verify an access token and return an auth context if valid.
471    fn verify(
472        &self,
473        ctx: &McpContext,
474        request: AuthRequest<'_>,
475        token: &AccessToken,
476    ) -> McpResult<AuthContext>;
477}
478
479/// Token-based authentication provider.
480#[derive(Clone)]
481pub struct TokenAuthProvider {
482    verifier: Arc<dyn TokenVerifier>,
483    missing_token_error: McpError,
484}
485
486impl TokenAuthProvider {
487    /// Creates a new token auth provider with the given verifier.
488    #[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    /// Overrides the error returned when a token is missing.
497    #[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
513/// Static token verifier backed by fixed-width token digests.
514pub 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    /// Creates a new static verifier from a token → context map.
533    ///
534    /// Every configured credential must resolve to an admissible authenticated
535    /// owner. Rejecting ownerless or oversized facts here prevents a verifier
536    /// configuration that succeeds in isolation but is guaranteed to fail at
537    /// the server's authentication-admission boundary.
538    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    /// Restricts accepted token schemes (case-insensitive).
576    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/// Default allow-all provider (returns anonymous auth context).
641#[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// =============================================================================
651// Tests
652// =============================================================================
653
654#[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        // Bare token parsing treats the entire value as a bearer token, even if it
680        // happens to be the literal string "Bearer".
681        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        // params as string
698        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        // params as object with authorization field
713        let params = serde_json::json!({"authorization": "Bearer t2"});
714        let req = AuthRequest {
715            method: "tools/call",
716            params: Some(&params),
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        // params as object with {scheme, token}
729        let params = serde_json::json!({"auth": {"scheme": "Bearer", "token": "t3"}});
730        let req = AuthRequest {
731            method: "tools/call",
732            params: Some(&params),
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        // params as object with _meta.authorization
745        let params = serde_json::json!({"_meta": {"authorization": "Bearer t4"}});
746        let req = AuthRequest {
747            method: "tools/call",
748            params: Some(&params),
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        // params as object with headers.Authorization
761        let params = serde_json::json!({"headers": {"Authorization": "Bearer t5"}});
762        let req = AuthRequest {
763            method: "tools/call",
764            params: Some(&params),
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(&params),
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(&params),
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(&params),
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        // Wrong scheme
1013        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        // Valid scheme (case-insensitive)
1026        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(&params),
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(&params),
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(&params),
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(&params),
1111            transport_authorization: None,
1112            request_id: 1,
1113        };
1114        let token = req.access_token().expect("should extract");
1115        // Bare token defaults to Bearer scheme
1116        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(&params),
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(&params),
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        // Whitespace-only token should be rejected by the scheme/token path
1148        // and also by the legacy in-band parser, which trims empty values.
1149        let params = serde_json::json!({"authorization": "  "});
1150        let req = AuthRequest {
1151            method: "tools/call",
1152            params: Some(&params),
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(&params),
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(&params),
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(&params),
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; // Copy
1308        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        // headers containing an object with scheme and token
1315        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(&params),
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(&params),
1337            transport_authorization: None,
1338            request_id: 1,
1339        };
1340        assert!(req.access_token().is_none());
1341    }
1342
1343    // ── AllowAllAuthProvider derives ─────────────────────────────────
1344
1345    #[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; // Copy
1362        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    // ── TokenAuthProvider ────────────────────────────────────────────
1377
1378    #[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(&params),
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(&params),
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    // ── StaticTokenVerifier ──────────────────────────────────────────
1414
1415    #[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        // Bearer works
1509        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        // Token works
1522        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        // Basic does not
1535        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    // ── extract_from_value edge cases ────────────────────────────────
1550
1551    #[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(&params),
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(&params),
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        // Object with auth field pointing to object that has an inner authorization string
1578        let params = serde_json::json!({
1579            "auth": {
1580                "authorization": "Bearer inner-tok"
1581            }
1582        });
1583        let req = AuthRequest {
1584            method: "test",
1585            params: Some(&params),
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    // ── _meta and headers fallback priority ──────────────────────────
1594
1595    #[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(&params),
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(&params),
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(&params),
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(&params),
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    // ── auth_error helper ────────────────────────────────────────────
1674
1675    #[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    // ── extract_from_value with non-matching object ──────────────────
1683
1684    #[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(&params),
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(&params),
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    // ── _meta / headers non-object fallthrough ──────────────────────
1710
1711    #[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(&params),
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(&params),
1736            transport_authorization: None,
1737            request_id: 1,
1738        };
1739        assert!(req.access_token().is_none());
1740    }
1741
1742    // ── Non-string, non-object values in map fields ─────────────────
1743
1744    #[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(&params),
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(&params),
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(&params),
1774            transport_authorization: None,
1775            request_id: 1,
1776        };
1777        assert!(req.access_token().is_none());
1778    }
1779
1780    // ── extract_from_value nested accessToken key ───────────────────
1781
1782    #[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(&params),
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    // ── StaticTokenVerifier with empty allowed_schemes ──────────────
1802
1803    #[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    // ── TokenAuthProvider with scheme restriction in verifier ────────
1902
1903    #[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        // Basic scheme rejected by verifier
1912        let params = serde_json::json!({"authorization": "Basic secret"});
1913        let req = AuthRequest {
1914            method: "test",
1915            params: Some(&params),
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        // Bearer scheme accepted
1923        let params = serde_json::json!({"authorization": "Bearer secret"});
1924        let req = AuthRequest {
1925            method: "test",
1926            params: Some(&params),
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    // ── AuthRequest with all fields populated ───────────────────────
1935
1936    #[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(&params),
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}