use std::collections::HashMap;
use std::io::Write as _;
use std::sync::Arc;
use fastmcp_core::{
AccessToken, AuthContext, MAX_ACCESS_TOKEN_BYTES, McpContext, McpError, McpErrorCode,
McpResult, Sha256Digest, sha256_bounded,
};
const ACCESS_TOKEN_FIELDS: [&str; 6] = [
"authorization",
"Authorization",
"auth",
"token",
"access_token",
"accessToken",
];
const MAX_AUTH_SUBJECT_BYTES: usize = 1024;
const MAX_AUTH_SCOPES: usize = 64;
const MAX_AUTH_SCOPE_BYTES: usize = 256;
const MAX_AUTH_CLAIM_NODES: usize = 1024;
const MAX_AUTH_CLAIM_DEPTH: usize = 32;
const MAX_AUTH_CLAIM_STRING_BYTES: usize = 16 * 1024;
const MAX_AUTH_CONTEXT_BYTES: usize = 64 * 1024;
const AUTH_BYTES_GROWTH_CHUNK: usize = 4 * 1024;
const MAX_STATIC_TOKEN_ENTRIES: usize = 4_096;
const MAX_ALLOWED_AUTH_SCHEMES: usize = 16;
const AUTHENTICATED_PRINCIPAL_DOMAIN: &[u8] = b"fastmcp/session-principal/authenticated/v1\0";
const ANONYMOUS_PRINCIPAL_DOMAIN: &[u8] = b"fastmcp/session-principal/anonymous/v1\0";
struct BoundedAuthBytes {
bytes: Vec<u8>,
}
impl BoundedAuthBytes {
fn new() -> Self {
Self { bytes: Vec::new() }
}
fn ensure_capacity_for(&mut self, next_size: usize) -> std::io::Result<()> {
if next_size <= self.bytes.capacity() {
return Ok(());
}
let current_capacity = self.bytes.capacity();
let geometric_target = if current_capacity == 0 {
AUTH_BYTES_GROWTH_CHUNK
} else {
current_capacity
.checked_mul(2)
.unwrap_or(MAX_AUTH_CONTEXT_BYTES)
.min(MAX_AUTH_CONTEXT_BYTES)
};
let target_capacity = next_size.max(geometric_target).min(MAX_AUTH_CONTEXT_BYTES);
let mut grown = Vec::new();
grown
.try_reserve_exact(target_capacity)
.map_err(|_| std::io::Error::other("authentication context allocation failed"))?;
if grown.capacity() > MAX_AUTH_CONTEXT_BYTES {
return Err(std::io::Error::other(
"authentication context allocation exceeds byte limit",
));
}
grown.extend_from_slice(&self.bytes);
self.bytes = grown;
Ok(())
}
}
impl std::io::Write for BoundedAuthBytes {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
let new_len = self
.bytes
.len()
.checked_add(buffer.len())
.filter(|length| *length <= MAX_AUTH_CONTEXT_BYTES)
.ok_or_else(|| std::io::Error::other("authentication context exceeds byte limit"))?;
self.ensure_capacity_for(new_len)?;
self.bytes.extend_from_slice(buffer);
Ok(buffer.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn claims_shape_is_bounded(root: &serde_json::Value) -> bool {
let mut stack = Vec::new();
if stack.try_reserve_exact(1).is_err() {
return false;
}
stack.push((root, 0_usize));
let mut nodes = 0_usize;
while let Some((value, depth)) = stack.pop() {
nodes = match nodes.checked_add(1) {
Some(nodes) if nodes <= MAX_AUTH_CLAIM_NODES => nodes,
_ => return false,
};
if depth > MAX_AUTH_CLAIM_DEPTH {
return false;
}
match value {
serde_json::Value::String(value) => {
if value.len() > MAX_AUTH_CLAIM_STRING_BYTES {
return false;
}
}
serde_json::Value::Array(values) => {
for value in values {
if nodes.saturating_add(stack.len()) >= MAX_AUTH_CLAIM_NODES
|| stack.try_reserve(1).is_err()
{
return false;
}
stack.push((value, depth.saturating_add(1)));
}
}
serde_json::Value::Object(values) => {
for (key, value) in values {
if key.len() > MAX_AUTH_CLAIM_STRING_BYTES
|| nodes.saturating_add(stack.len()) >= MAX_AUTH_CLAIM_NODES
|| stack.try_reserve(1).is_err()
{
return false;
}
stack.push((value, depth.saturating_add(1)));
}
}
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {
}
}
}
true
}
pub(crate) fn principal_fingerprint(auth: Option<&AuthContext>) -> McpResult<Sha256Digest> {
let mut canonical = BoundedAuthBytes::new();
match auth {
None => canonical
.write_all(ANONYMOUS_PRINCIPAL_DOMAIN)
.map_err(|_| McpError::internal_error("authentication admission exceeds bounds"))?,
Some(auth) => {
if auth
.subject
.as_ref()
.is_some_and(|subject| subject.is_empty() || subject.len() > MAX_AUTH_SUBJECT_BYTES)
|| auth.scopes.len() > MAX_AUTH_SCOPES
|| auth
.scopes
.iter()
.any(|scope| scope.is_empty() || scope.len() > MAX_AUTH_SCOPE_BYTES)
|| auth
.claims
.as_ref()
.is_some_and(|claims| !claims_shape_is_bounded(claims))
{
return Err(McpError::internal_error(
"authentication provider returned facts outside admission bounds",
));
}
let mut admitted_facts = BoundedAuthBytes::new();
serde_json::to_writer(&mut admitted_facts, auth).map_err(|_| {
McpError::internal_error(
"authentication provider returned facts outside admission bounds",
)
})?;
canonical
.write_all(AUTHENTICATED_PRINCIPAL_DOMAIN)
.map_err(|_| McpError::internal_error("authentication admission exceeds bounds"))?;
match (auth.session_owner(), auth.subject.as_deref()) {
(Some(owner), _) => {
canonical.write_all(&[2]).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
canonical.write_all(owner.as_bytes()).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
}
(None, None) if auth.scopes.is_empty() && auth.claims.is_none() => {
canonical.write_all(&[0]).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
}
(None, None) => {
return Err(McpError::internal_error(
"authentication provider returned ownerless authorization facts",
));
}
(None, Some(subject)) => {
canonical.write_all(&[1]).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
let length = u64::try_from(subject.len()).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
canonical.write_all(&length.to_be_bytes()).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
canonical.write_all(subject.as_bytes()).map_err(|_| {
McpError::internal_error("authentication admission exceeds bounds")
})?;
}
}
}
}
sha256_bounded(&canonical.bytes, MAX_AUTH_CONTEXT_BYTES)
.map_err(|_| McpError::internal_error("authentication admission exceeds bounds"))
}
#[derive(Clone, Copy)]
pub struct AuthRequest<'a> {
pub method: &'a str,
pub params: Option<&'a serde_json::Value>,
pub transport_authorization: Option<&'a str>,
pub request_id: u64,
}
impl std::fmt::Debug for AuthRequest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthRequest")
.field("method_bytes", &self.method.len())
.field("params_present", &self.params.is_some())
.field(
"transport_authorization_present",
&self.transport_authorization.is_some(),
)
.field("request_id", &self.request_id)
.finish()
}
}
impl AuthRequest<'_> {
#[must_use]
pub fn access_token(&self) -> Option<AccessToken> {
admitted_access_token(*self).ok().flatten()
}
#[must_use]
pub fn has_multiple_credential_sources(&self) -> bool {
matches!(
admitted_access_token(*self),
Err(CredentialSourceError::Multiple)
)
}
pub(crate) fn credential_sources_are_admissible(&self) -> bool {
admitted_access_token(*self).is_ok()
}
pub(crate) fn has_any_credential_source(&self) -> bool {
self.transport_authorization.is_some() || single_in_band_credential(self.params).is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CredentialSourceError {
Malformed,
Multiple,
}
fn admitted_access_token(
request: AuthRequest<'_>,
) -> Result<Option<AccessToken>, CredentialSourceError> {
let in_band = single_in_band_credential(request.params);
match request.transport_authorization {
Some(_) if in_band.is_some() => Err(CredentialSourceError::Multiple),
Some(authorization) => parse_native_authorization(authorization)
.map(Some)
.ok_or(CredentialSourceError::Malformed),
None => match in_band.transpose()? {
Some(value) => extract_from_value(value).map(Some),
None => Ok(None),
},
}
}
fn record_credential_candidate<'a>(
candidate: &mut Option<&'a serde_json::Value>,
value: &'a serde_json::Value,
) -> Result<(), CredentialSourceError> {
if candidate.replace(value).is_some() {
return Err(CredentialSourceError::Multiple);
}
Ok(())
}
fn scan_credential_map<'a>(
map: &'a serde_json::Map<String, serde_json::Value>,
candidate: &mut Option<&'a serde_json::Value>,
) -> Result<(), CredentialSourceError> {
for key in ACCESS_TOKEN_FIELDS {
if let Some(value) = map.get(key) {
record_credential_candidate(candidate, value)?;
}
}
Ok(())
}
fn single_in_band_credential(
params: Option<&serde_json::Value>,
) -> Option<Result<&serde_json::Value, CredentialSourceError>> {
let params = params?;
if matches!(params, serde_json::Value::String(_)) {
return Some(Ok(params));
}
let serde_json::Value::Object(map) = params else {
return None;
};
let mut candidate = None;
if let Err(error) = scan_credential_map(map, &mut candidate) {
return Some(Err(error));
}
for container in ["_meta", "headers"] {
if let Some(nested) = map.get(container).and_then(serde_json::Value::as_object)
&& let Err(error) = scan_credential_map(nested, &mut candidate)
{
return Some(Err(error));
}
}
candidate.map(Ok)
}
fn parse_native_authorization(value: &str) -> Option<AccessToken> {
AccessToken::parse(value)
}
fn extract_from_value(value: &serde_json::Value) -> Result<AccessToken, CredentialSourceError> {
match value {
serde_json::Value::String(value) => {
AccessToken::parse_legacy_in_band(value).ok_or(CredentialSourceError::Malformed)
}
serde_json::Value::Object(map) => {
let scheme = map.get("scheme");
let token = map.get("token");
let alternative_keys = [
"authorization",
"Authorization",
"access_token",
"accessToken",
];
let alternative_count = alternative_keys
.iter()
.filter(|key| map.contains_key(**key))
.count();
if scheme.is_some() || token.is_some() {
if alternative_count != 0 {
return Err(CredentialSourceError::Multiple);
}
let scheme = scheme
.and_then(serde_json::Value::as_str)
.ok_or(CredentialSourceError::Malformed)?;
let token = token
.and_then(serde_json::Value::as_str)
.ok_or(CredentialSourceError::Malformed)?;
let access = AccessToken::from_parts(scheme, token)
.ok_or(CredentialSourceError::Malformed)?;
return (access.scheme == scheme && access.token == token)
.then_some(access)
.ok_or(CredentialSourceError::Malformed);
}
if alternative_count > 1 {
return Err(CredentialSourceError::Multiple);
}
if alternative_count == 0 {
return Err(CredentialSourceError::Malformed);
}
let value = alternative_keys
.iter()
.find_map(|key| map.get(*key))
.and_then(serde_json::Value::as_str)
.ok_or(CredentialSourceError::Malformed)?;
AccessToken::parse_legacy_in_band(value).ok_or(CredentialSourceError::Malformed)
}
_ => Err(CredentialSourceError::Malformed),
}
}
pub(crate) fn strip_recognized_access_credentials(params: &mut Option<serde_json::Value>) {
if matches!(params, Some(serde_json::Value::String(_))) {
*params = None;
return;
}
let Some(serde_json::Value::Object(map)) = params.as_mut() else {
return;
};
remove_access_token_fields(map);
for container in ["_meta", "headers"] {
if let Some(nested) = map
.get_mut(container)
.and_then(serde_json::Value::as_object_mut)
{
remove_access_token_fields(nested);
}
}
}
fn remove_access_token_fields(map: &mut serde_json::Map<String, serde_json::Value>) {
for key in ACCESS_TOKEN_FIELDS {
map.remove(key);
}
}
pub trait AuthProvider: Send + Sync {
fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext>;
}
pub trait TokenVerifier: Send + Sync {
fn verify(
&self,
ctx: &McpContext,
request: AuthRequest<'_>,
token: &AccessToken,
) -> McpResult<AuthContext>;
}
#[derive(Clone)]
pub struct TokenAuthProvider {
verifier: Arc<dyn TokenVerifier>,
missing_token_error: McpError,
}
impl TokenAuthProvider {
#[must_use]
pub fn new<V: TokenVerifier + 'static>(verifier: V) -> Self {
Self {
verifier: Arc::new(verifier),
missing_token_error: auth_error("Missing access token"),
}
}
#[must_use]
pub fn with_missing_token_error(mut self, error: McpError) -> Self {
self.missing_token_error = error;
self
}
}
impl AuthProvider for TokenAuthProvider {
fn authenticate(&self, ctx: &McpContext, request: AuthRequest<'_>) -> McpResult<AuthContext> {
let access = request
.access_token()
.ok_or_else(|| self.missing_token_error.clone())?;
self.verifier.verify(ctx, request, &access)
}
}
pub struct StaticTokenVerifier {
tokens: HashMap<Sha256Digest, AuthContext>,
allowed_schemes: Option<Vec<String>>,
}
impl std::fmt::Debug for StaticTokenVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticTokenVerifier")
.field("token_count", &self.tokens.len())
.field(
"allowed_scheme_count",
&self.allowed_schemes.as_ref().map_or(0, Vec::len),
)
.finish()
}
}
impl StaticTokenVerifier {
pub fn new<I, K>(tokens: I) -> McpResult<Self>
where
I: IntoIterator<Item = (K, AuthContext)>,
K: Into<String>,
{
let mut digests = HashMap::new();
for (token, context) in tokens {
if digests.len() >= MAX_STATIC_TOKEN_ENTRIES {
return Err(auth_error("Static token configuration is invalid"));
}
if context.subject.as_deref().is_none_or(str::is_empty)
|| principal_fingerprint(Some(&context)).is_err()
{
return Err(auth_error("Static token configuration is invalid"));
}
let token = token.into();
if !AccessToken::is_valid_token68(&token) {
return Err(auth_error("Static token configuration is invalid"));
}
let digest = sha256_bounded(token.as_bytes(), MAX_ACCESS_TOKEN_BYTES)
.map_err(|_| auth_error("Static token configuration is invalid"))?;
digests
.try_reserve(1)
.map_err(|_| auth_error("Static token configuration is invalid"))?;
if digests.insert(digest, context).is_some() {
return Err(auth_error("Static token configuration is invalid"));
}
}
if digests.is_empty() {
return Err(auth_error("Static token configuration is invalid"));
}
Ok(Self {
tokens: digests,
allowed_schemes: None,
})
}
pub fn with_allowed_schemes<I, S>(mut self, schemes: I) -> McpResult<Self>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut admitted = Vec::new();
for scheme in schemes {
if admitted.len() >= MAX_ALLOWED_AUTH_SCHEMES {
return Err(auth_error("Static auth scheme configuration is invalid"));
}
let scheme = scheme.into();
if !AccessToken::is_valid_http_scheme(&scheme) {
return Err(auth_error("Static auth scheme configuration is invalid"));
}
let normalized = scheme.to_ascii_lowercase();
if admitted.iter().any(|existing| existing == &normalized) {
return Err(auth_error("Static auth scheme configuration is invalid"));
}
admitted
.try_reserve(1)
.map_err(|_| auth_error("Static auth scheme configuration is invalid"))?;
admitted.push(normalized);
}
if admitted.is_empty() {
return Err(auth_error("Static auth scheme configuration is invalid"));
}
self.allowed_schemes = Some(admitted);
Ok(self)
}
}
impl TokenVerifier for StaticTokenVerifier {
fn verify(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
token: &AccessToken,
) -> McpResult<AuthContext> {
if !AccessToken::is_valid_http_scheme(&token.scheme)
|| !AccessToken::is_valid_token68(&token.token)
{
return Err(auth_error("Invalid access token"));
}
if let Some(allowed) = &self.allowed_schemes {
let normalized = token.scheme.to_ascii_lowercase();
if !allowed.contains(&normalized) {
return Err(auth_error("Unsupported auth scheme"));
}
}
let digest = sha256_bounded(token.token.as_bytes(), MAX_ACCESS_TOKEN_BYTES)
.map_err(|_| auth_error("Invalid access token"))?;
let Some(auth) = self.tokens.get(&digest) else {
return Err(auth_error("Invalid access token"));
};
Ok(auth.clone())
}
}
fn auth_error(message: impl Into<String>) -> McpError {
McpError::new(McpErrorCode::ResourceForbidden, message)
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AllowAllAuthProvider;
impl AuthProvider for AllowAllAuthProvider {
fn authenticate(&self, _ctx: &McpContext, _request: AuthRequest<'_>) -> McpResult<AuthContext> {
Ok(AuthContext::anonymous())
}
}
#[cfg(test)]
mod tests {
use super::*;
use asupersync::Cx;
fn ctx() -> McpContext {
McpContext::new(Cx::for_testing(), 1)
}
#[test]
fn access_token_parsers_separate_http_and_legacy_in_band_grammar() {
assert_eq!(
fastmcp_core::AccessToken::parse("Bearer abc"),
Some(fastmcp_core::AccessToken {
scheme: "Bearer".to_string(),
token: "abc".to_string(),
})
);
assert_eq!(
fastmcp_core::AccessToken::parse_legacy_in_band("abc"),
Some(fastmcp_core::AccessToken {
scheme: "Bearer".to_string(),
token: "abc".to_string(),
})
);
assert_eq!(
fastmcp_core::AccessToken::parse_legacy_in_band(" Bearer"),
Some(fastmcp_core::AccessToken {
scheme: "Bearer".to_string(),
token: "Bearer".to_string(),
})
);
assert_eq!(fastmcp_core::AccessToken::parse(""), None);
assert_eq!(fastmcp_core::AccessToken::parse(" "), None);
assert_eq!(fastmcp_core::AccessToken::parse("Bearer "), None);
assert_eq!(fastmcp_core::AccessToken::parse("abc"), None);
assert_eq!(fastmcp_core::AccessToken::parse("Bearer abc:def"), None);
}
#[test]
fn auth_request_extracts_access_token_from_common_locations() {
let req = AuthRequest {
method: "tools/call",
params: Some(&serde_json::Value::String("Bearer t1".to_string())),
transport_authorization: None,
request_id: 1,
};
assert_eq!(
req.access_token(),
Some(AccessToken {
scheme: "Bearer".to_string(),
token: "t1".to_string(),
})
);
let params = serde_json::json!({"authorization": "Bearer t2"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert_eq!(
req.access_token(),
Some(AccessToken {
scheme: "Bearer".to_string(),
token: "t2".to_string(),
})
);
let params = serde_json::json!({"auth": {"scheme": "Bearer", "token": "t3"}});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert_eq!(
req.access_token(),
Some(AccessToken {
scheme: "Bearer".to_string(),
token: "t3".to_string(),
})
);
let params = serde_json::json!({"_meta": {"authorization": "Bearer t4"}});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert_eq!(
req.access_token(),
Some(AccessToken {
scheme: "Bearer".to_string(),
token: "t4".to_string(),
})
);
let params = serde_json::json!({"headers": {"Authorization": "Bearer t5"}});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert_eq!(
req.access_token(),
Some(AccessToken {
scheme: "Bearer".to_string(),
token: "t5".to_string(),
})
);
}
#[test]
fn native_and_in_band_authorization_are_rejected_as_ambiguous() {
let params = serde_json::json!({"authorization": "Bearer in-band"});
let request = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: Some("Token native"),
request_id: 1,
};
assert!(request.access_token().is_none());
assert!(request.has_multiple_credential_sources());
}
#[test]
fn malformed_native_authorization_cannot_fall_back_in_band() {
let params = serde_json::json!({"authorization": "Bearer in-band"});
let request = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: Some("Bearer "),
request_id: 1,
};
assert!(request.access_token().is_none());
}
#[test]
fn native_authorization_requires_http_token_scheme_and_token68_credential() {
for malformed in [
"Bearer",
"Basic",
"Bearer one two",
"Bearer\tcredential",
"Be(arer token",
"Bearer ab=c",
"Bearer credential,other",
" Bearer credential",
"Bearer credential ",
] {
let request = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: Some(malformed),
request_id: 1,
};
assert!(
request.access_token().is_none(),
"accepted malformed native authorization: {malformed:?}"
);
assert!(!request.credential_sources_are_admissible());
}
let request = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: Some("Bearer abc_DEF-123+/=="),
request_id: 1,
};
let token = request.access_token().expect("valid native authorization");
assert_eq!(token.scheme, "Bearer");
assert_eq!(token.token, "abc_DEF-123+/==");
}
#[test]
fn normalized_http_authorization_still_uses_strict_native_grammar() {
use fastmcp_transport::http::HttpTransport;
use std::io::Cursor;
for (wire_value, accepted) in [
("Bearer ", false),
("Basic", false),
("Bearer\tcredential", false),
("Bearer one two", false),
("Bearer abc_DEF-123==", true),
] {
let wire = format!(
"POST /mcp HTTP/1.1\r\nHost: localhost\r\nAuthorization: {wire_value}\r\nContent-Length: 0\r\n\r\n"
);
let mut transport = HttpTransport::new(Cursor::new(wire.into_bytes()), Vec::new());
let http_request = transport.read_request().expect("parse HTTP request");
let auth_request = AuthRequest {
method: "tools/list",
params: None,
transport_authorization: http_request.authorization(),
request_id: 1,
};
assert_eq!(
auth_request.access_token().is_some(),
accepted,
"unexpected admission for normalized Authorization {wire_value:?}"
);
}
}
#[test]
fn auth_request_detects_multiple_credential_sources() {
let params = serde_json::json!({"_meta": {"accessToken": "Bearer in-band"}});
let multiple = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: Some("Bearer native"),
request_id: 1,
};
assert!(multiple.has_multiple_credential_sources());
let application_params = serde_json::json!({"arguments": {"token": "application-data"}});
let native_only = AuthRequest {
method: "tools/call",
params: Some(&application_params),
transport_authorization: Some("Bearer native"),
request_id: 2,
};
assert!(!native_only.has_multiple_credential_sources());
}
#[test]
fn principal_fingerprint_separates_anonymous_from_authenticated_facts() {
let anonymous = principal_fingerprint(None).expect("anonymous fingerprint");
let admitted_anonymous =
principal_fingerprint(Some(&AuthContext::anonymous())).expect("provider fingerprint");
let alice =
principal_fingerprint(Some(&AuthContext::with_subject("alice"))).expect("fingerprint");
let alice_again =
principal_fingerprint(Some(&AuthContext::with_subject("alice"))).expect("fingerprint");
assert_ne!(anonymous, admitted_anonymous);
assert_ne!(anonymous, alice);
assert_ne!(admitted_anonymous, alice);
assert_eq!(alice, alice_again);
let mut alice_with_changed_authorization = AuthContext::with_subject("alice");
alice_with_changed_authorization.scopes = vec!["write".to_string(), "read".to_string()];
alice_with_changed_authorization.claims =
Some(serde_json::json!({"exp": 99, "policy_revision": 2}));
assert_eq!(
alice,
principal_fingerprint(Some(&alice_with_changed_authorization))
.expect("stable owner fingerprint")
);
assert_ne!(
alice,
principal_fingerprint(Some(&AuthContext::with_subject("bob")))
.expect("different owner fingerprint")
);
}
#[test]
fn principal_fingerprint_rejects_out_of_bounds_provider_facts() {
let oversized_subject = AuthContext::with_subject("s".repeat(MAX_AUTH_SUBJECT_BYTES + 1));
assert!(principal_fingerprint(Some(&oversized_subject)).is_err());
assert!(principal_fingerprint(Some(&AuthContext::with_subject(""))).is_err());
let mut empty_scope = AuthContext::anonymous();
empty_scope.scopes = vec![String::new()];
assert!(principal_fingerprint(Some(&empty_scope)).is_err());
let mut too_many_scopes = AuthContext::anonymous();
too_many_scopes.scopes = vec!["scope".to_string(); MAX_AUTH_SCOPES + 1];
assert!(principal_fingerprint(Some(&too_many_scopes)).is_err());
let mut oversized_claim = AuthContext::anonymous();
oversized_claim.claims = Some(serde_json::Value::String(
"c".repeat(MAX_AUTH_CLAIM_STRING_BYTES + 1),
));
assert!(principal_fingerprint(Some(&oversized_claim)).is_err());
let mut oversized_aggregate = AuthContext::with_subject("owner");
oversized_aggregate.claims = Some(serde_json::Value::Array(vec![
serde_json::Value::String(
"c".repeat(MAX_AUTH_CLAIM_STRING_BYTES)
);
5
]));
assert!(claims_shape_is_bounded(
oversized_aggregate.claims.as_ref().unwrap()
));
assert!(principal_fingerprint(Some(&oversized_aggregate)).is_err());
}
#[test]
fn token_auth_provider_errors_on_missing_token_and_allows_override() {
#[derive(Debug)]
struct AcceptAll;
impl TokenVerifier for AcceptAll {
fn verify(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
_token: &AccessToken,
) -> McpResult<AuthContext> {
Ok(AuthContext::with_subject("ok"))
}
}
let provider = TokenAuthProvider::new(AcceptAll);
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
let err = provider.authenticate(&ctx(), req).unwrap_err();
assert_eq!(err.code, McpErrorCode::ResourceForbidden);
assert!(err.message.contains("Missing access token"));
let provider =
TokenAuthProvider::new(AcceptAll).with_missing_token_error(auth_error("no token"));
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
let err = provider.authenticate(&ctx(), req).unwrap_err();
assert!(err.message.contains("no token"));
}
#[test]
fn static_token_verifier_enforces_scheme_without_exposing_token() {
let mut base = AuthContext::with_subject("user123");
base.scopes = vec!["read".to_string()];
let verifier = StaticTokenVerifier::new([("value-1", base.clone())])
.expect("valid verifier configuration")
.with_allowed_schemes(["Bearer"])
.expect("valid scheme configuration");
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
let err = verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Basic".to_string(),
token: "value-1".to_string(),
},
)
.unwrap_err();
assert!(err.message.contains("Unsupported auth scheme"));
let auth = verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "bearer".to_string(),
token: "value-1".to_string(),
},
)
.unwrap();
assert_eq!(auth.subject, Some("user123".to_string()));
assert_eq!(auth.scopes, vec!["read".to_string()]);
let serialized = serde_json::to_string(&auth).expect("serialize verified auth facts");
assert!(!serialized.contains("value-1"));
}
#[test]
fn allow_all_provider_returns_anonymous_context() {
let provider = AllowAllAuthProvider;
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
let auth = provider.authenticate(&ctx(), req).unwrap();
assert_eq!(auth.subject, None);
assert!(auth.scopes.is_empty());
}
#[test]
fn access_token_from_none_params() {
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_from_array_params() {
let params = serde_json::json!([1, 2, 3]);
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_from_number_params() {
let params = serde_json::json!(42);
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_from_object_with_token_field() {
let params = serde_json::json!({"token": "Bearer my-secret"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should extract token");
assert_eq!(token.scheme, "Bearer");
assert_eq!(token.token, "my-secret");
}
#[test]
fn access_token_from_object_with_access_token_field() {
let params = serde_json::json!({"access_token": "abc123"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should extract");
assert_eq!(token.scheme, "Bearer");
assert_eq!(token.token, "abc123");
}
#[test]
fn access_token_from_camel_case_field() {
let params = serde_json::json!({"accessToken": "Bearer xyz"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should extract");
assert_eq!(token.token, "xyz");
}
#[test]
fn access_token_from_nested_scheme_token_object_rejects_empty_scheme() {
let params = serde_json::json!({"auth": {"scheme": "", "token": "abc"}});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert_eq!(req.access_token(), None);
}
#[test]
fn access_token_from_nested_scheme_token_object_with_whitespace_token() {
let params = serde_json::json!({"authorization": " "});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn static_verifier_rejects_unknown_token() {
let verifier =
StaticTokenVerifier::new([("valid-token", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration");
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
let err = verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Bearer".to_string(),
token: "wrong-token".to_string(),
},
)
.unwrap_err();
assert_eq!(err.code, McpErrorCode::ResourceForbidden);
assert!(err.message.contains("Invalid access token"));
}
#[test]
fn static_verifier_no_scheme_restriction_allows_any() {
let verifier = StaticTokenVerifier::new([("tok", AuthContext::with_subject("alice"))])
.expect("valid verifier configuration");
let req = AuthRequest {
method: "tools/call",
params: None,
transport_authorization: None,
request_id: 1,
};
let auth = verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "CustomScheme".to_string(),
token: "tok".to_string(),
},
)
.unwrap();
assert_eq!(auth.subject, Some("alice".to_string()));
}
#[test]
fn token_auth_provider_succeeds_with_valid_token() {
let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("bob"))])
.expect("valid verifier configuration");
let provider = TokenAuthProvider::new(verifier);
let params = serde_json::json!({"authorization": "Bearer secret"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let auth = provider.authenticate(&ctx(), req).unwrap();
assert_eq!(auth.subject, Some("bob".to_string()));
}
#[test]
fn token_auth_provider_fails_with_wrong_token() {
let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("bob"))])
.expect("valid verifier configuration");
let provider = TokenAuthProvider::new(verifier);
let params = serde_json::json!({"authorization": "Bearer wrong"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let err = provider.authenticate(&ctx(), req).unwrap_err();
assert_eq!(err.code, McpErrorCode::ResourceForbidden);
}
#[test]
fn auth_request_debug() {
let params = serde_json::json!({"AUTH_PARAMS_DEBUG_CANARY": "AUTH_VALUE_DEBUG_CANARY"});
let req = AuthRequest {
method: "AUTH_METHOD_DEBUG_CANARY",
params: Some(¶ms),
transport_authorization: None,
request_id: 42,
};
let debug = format!("{req:?}");
assert!(debug.contains("method_bytes"));
assert!(debug.contains("42"));
assert!(!debug.contains("AUTH_METHOD_DEBUG_CANARY"));
assert!(!debug.contains("AUTH_PARAMS_DEBUG_CANARY"));
assert!(!debug.contains("AUTH_VALUE_DEBUG_CANARY"));
}
#[test]
fn credential_stripping_removes_only_recognized_locations() {
let mut params = Some(serde_json::json!({
"authorization": "Bearer top-secret",
"auth": {"token": "nested-top-secret"},
"_meta": {
"accessToken": "meta-secret",
"trace": "keep-meta"
},
"headers": {
"Authorization": "Bearer header-secret",
"content-type": "application/json"
},
"arguments": {
"token": "application-data",
"nested": {"authorization": "application-data-too"}
},
"name": "tool-name"
}));
strip_recognized_access_credentials(&mut params);
assert_eq!(
params,
Some(serde_json::json!({
"_meta": {"trace": "keep-meta"},
"headers": {"content-type": "application/json"},
"arguments": {
"token": "application-data",
"nested": {"authorization": "application-data-too"}
},
"name": "tool-name"
}))
);
}
#[test]
fn credential_stripping_removes_legacy_bare_string_payload() {
let mut params = Some(serde_json::json!("Bearer secret"));
strip_recognized_access_credentials(&mut params);
assert_eq!(params, None);
}
#[test]
fn auth_request_clone_copy() {
let req = AuthRequest {
method: "test",
params: None,
transport_authorization: None,
request_id: 1,
};
let req2 = req; assert_eq!(req.method, req2.method);
assert_eq!(req.request_id, req2.request_id);
}
#[test]
fn access_token_from_headers_nested_object() {
let params = serde_json::json!({
"headers": {
"Authorization": {"scheme": "Bearer", "token": "hdr-tok"}
}
});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should extract from headers");
assert_eq!(token.scheme, "Bearer");
assert_eq!(token.token, "hdr-tok");
}
#[test]
fn access_token_from_empty_object() {
let params = serde_json::json!({});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn allow_all_provider_debug() {
let provider = AllowAllAuthProvider;
let debug = format!("{provider:?}");
assert!(debug.contains("AllowAllAuthProvider"));
}
#[test]
fn allow_all_provider_default() {
let _ = AllowAllAuthProvider;
}
#[test]
fn allow_all_provider_clone_copy() {
let provider = AllowAllAuthProvider;
let cloned = provider.clone();
let copied = provider; let _ = cloned
.authenticate(
&ctx(),
AuthRequest {
method: "test",
params: None,
transport_authorization: None,
request_id: 1,
},
)
.unwrap();
let _ = copied;
}
#[test]
fn token_auth_provider_clone() {
let verifier =
StaticTokenVerifier::new([("tok", AuthContext::with_subject("clone-owner"))])
.expect("valid verifier configuration");
let provider = TokenAuthProvider::new(verifier);
let cloned = provider.clone();
let params = serde_json::json!({"authorization": "Bearer tok"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let auth = cloned.authenticate(&ctx(), req).unwrap();
assert_eq!(auth.subject.as_deref(), Some("clone-owner"));
}
#[test]
fn token_auth_provider_with_custom_error_and_valid_token() {
let verifier = StaticTokenVerifier::new([("valid", AuthContext::with_subject("user"))])
.expect("valid verifier configuration");
let provider =
TokenAuthProvider::new(verifier).with_missing_token_error(auth_error("custom missing"));
let params = serde_json::json!({"authorization": "Bearer valid"});
let req = AuthRequest {
method: "tools/call",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let auth = provider.authenticate(&ctx(), req).unwrap();
assert_eq!(auth.subject, Some("user".to_string()));
}
#[test]
fn static_verifier_debug_redacts_configured_tokens() {
let canary = "STATIC_TOKEN_DEBUG_CANARY";
let verifier = StaticTokenVerifier::new([(canary, AuthContext::with_subject("owner"))])
.expect("valid verifier configuration");
let debug = format!("{verifier:?}");
assert!(debug.contains("StaticTokenVerifier"));
assert!(debug.contains("token_count"));
assert!(!debug.contains(canary));
}
#[test]
fn static_verifier_fails_closed_for_oversized_configured_or_presented_tokens() {
let oversized = "x".repeat(MAX_ACCESS_TOKEN_BYTES + 1);
let configured = StaticTokenVerifier::new([(
oversized.clone(),
AuthContext::with_subject("must-not-load"),
)]);
let config_error = configured.expect_err("oversized configured token must fail closed");
assert_eq!(config_error.code, McpErrorCode::ResourceForbidden);
assert!(!config_error.message.contains(&oversized));
let verifier = StaticTokenVerifier::new([("valid", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration");
let request = AuthRequest {
method: "test",
params: None,
transport_authorization: None,
request_id: 1,
};
let error = verifier
.verify(
&ctx(),
request,
&AccessToken {
scheme: "Bearer".to_string(),
token: oversized.clone(),
},
)
.expect_err("oversized presented token must fail closed");
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
assert!(!error.message.contains(&oversized));
}
#[test]
fn static_verifier_multiple_tokens() {
let verifier = StaticTokenVerifier::new([
("alpha", AuthContext::with_subject("alice")),
("beta", AuthContext::with_subject("bob")),
])
.expect("valid verifier configuration");
let req = AuthRequest {
method: "test",
params: None,
transport_authorization: None,
request_id: 1,
};
let a = verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Bearer".to_string(),
token: "alpha".to_string(),
},
)
.unwrap();
assert_eq!(a.subject, Some("alice".to_string()));
let b = verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Bearer".to_string(),
token: "beta".to_string(),
},
)
.unwrap();
assert_eq!(b.subject, Some("bob".to_string()));
}
#[test]
fn static_verifier_multiple_allowed_schemes() {
let verifier = StaticTokenVerifier::new([("tok", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration")
.with_allowed_schemes(["Bearer", "Token"])
.expect("valid scheme configuration");
let req = AuthRequest {
method: "test",
params: None,
transport_authorization: None,
request_id: 1,
};
assert!(
verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Bearer".to_string(),
token: "tok".to_string(),
},
)
.is_ok()
);
assert!(
verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Token".to_string(),
token: "tok".to_string(),
},
)
.is_ok()
);
assert!(
verifier
.verify(
&ctx(),
req,
&AccessToken {
scheme: "Basic".to_string(),
token: "tok".to_string(),
},
)
.is_err()
);
}
#[test]
fn access_token_from_bool_value_returns_none() {
let params = serde_json::json!(true);
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_from_null_params() {
let params = serde_json::json!(null);
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_from_nested_object_with_inner_authorization_string() {
let params = serde_json::json!({
"auth": {
"authorization": "Bearer inner-tok"
}
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should extract from nested auth");
assert_eq!(token.token, "inner-tok");
}
#[test]
fn access_token_meta_fallback_when_top_level_empty() {
let params = serde_json::json!({
"other_field": 123,
"_meta": {"authorization": "Bearer meta-tok"}
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should fallback to _meta");
assert_eq!(token.token, "meta-tok");
}
#[test]
fn access_token_headers_fallback_when_top_and_meta_empty() {
let params = serde_json::json!({
"other": "value",
"_meta": {"other": "value"},
"headers": {"authorization": "Bearer hdr-tok"}
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should fallback to headers");
assert_eq!(token.token, "hdr-tok");
}
#[test]
fn access_token_rejects_multiple_in_band_locations() {
let params = serde_json::json!({
"authorization": "Bearer top-tok",
"_meta": {"authorization": "Bearer meta-tok"}
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
assert!(req.has_multiple_credential_sources());
}
#[test]
fn access_token_reports_multiple_sources_inside_nested_credential_object() {
for params in [
serde_json::json!({
"auth": {
"authorization": "Bearer first",
"access_token": "Bearer second"
}
}),
serde_json::json!({
"auth": {
"scheme": "Bearer",
"token": "first",
"authorization": "Bearer second"
}
}),
] {
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
assert!(req.has_multiple_credential_sources());
assert!(!req.credential_sources_are_admissible());
}
}
#[test]
fn auth_error_creates_resource_forbidden() {
let err = auth_error("denied");
assert_eq!(err.code, McpErrorCode::ResourceForbidden);
assert!(err.message.contains("denied"));
}
#[test]
fn access_token_from_object_without_any_known_key() {
let params = serde_json::json!({"unknown_key": "Bearer tok"});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_from_scheme_token_with_whitespace_only_scheme() {
let params = serde_json::json!({"auth": {"scheme": " ", "token": "abc"}});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
assert!(!req.credential_sources_are_admissible());
}
#[test]
fn access_token_meta_non_object_falls_through_to_headers() {
let params = serde_json::json!({
"_meta": 42,
"headers": {"authorization": "Bearer hdr"}
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req.access_token().expect("should skip non-object _meta");
assert_eq!(token.token, "hdr");
}
#[test]
fn access_token_headers_non_object_returns_none() {
let params = serde_json::json!({
"_meta": {"other": true},
"headers": "not-an-object"
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_map_field_with_numeric_value_returns_none() {
let params = serde_json::json!({"authorization": 12345});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_map_field_with_bool_value_returns_none() {
let params = serde_json::json!({"token": true});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_map_field_with_array_value_returns_none() {
let params = serde_json::json!({"authorization": ["Bearer", "tok"]});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
assert!(req.access_token().is_none());
}
#[test]
fn access_token_nested_object_with_access_token_key() {
let params = serde_json::json!({
"auth": {
"accessToken": "Bearer nested-at"
}
});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let token = req
.access_token()
.expect("should extract from nested accessToken");
assert_eq!(token.token, "nested-at");
}
#[test]
fn static_verifier_rejects_empty_allowed_scheme_configuration() {
let error = StaticTokenVerifier::new([("tok", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration")
.with_allowed_schemes(Vec::<String>::new())
.expect_err("an empty scheme policy is a configuration error");
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
}
#[test]
fn static_verifier_rejects_malformed_and_duplicate_configuration() {
for context in [AuthContext::anonymous(), AuthContext::with_subject("")] {
let error = StaticTokenVerifier::new([("token", context)])
.expect_err("a configured credential must have a nonempty owner subject");
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
assert_eq!(error.message, "Static token configuration is invalid");
}
let oversized_subject = AuthContext::with_subject("x".repeat(MAX_AUTH_SUBJECT_BYTES + 1));
let error = StaticTokenVerifier::new([("token", oversized_subject)])
.expect_err("inadmissible authentication facts must fail at configuration time");
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
assert_eq!(error.message, "Static token configuration is invalid");
for token in ["", " ", " leading", "trailing ", "two words", "bad:token"] {
assert!(
StaticTokenVerifier::new([(token, AuthContext::with_subject("owner"))]).is_err(),
"accepted malformed static token {token:?}"
);
}
assert!(
StaticTokenVerifier::new([
("duplicate", AuthContext::with_subject("first")),
("duplicate", AuthContext::with_subject("second")),
])
.is_err()
);
let verifier = StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration");
assert!(verifier.with_allowed_schemes(["Bearer", "bearer"]).is_err());
let verifier = StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration");
assert!(verifier.with_allowed_schemes(["Bad Scheme"]).is_err());
}
#[test]
fn static_verifier_enforces_exact_entry_and_scheme_boundaries() {
let empty = StaticTokenVerifier::new(Vec::<(String, AuthContext)>::new())
.expect_err("an empty static-token map is not an authentication policy");
assert_eq!(empty.code, McpErrorCode::ResourceForbidden);
let maximum_entries = (0..MAX_STATIC_TOKEN_ENTRIES)
.map(|index| {
(
format!("token-{index}"),
AuthContext::with_subject(format!("owner-{index}")),
)
})
.collect::<Vec<_>>();
assert_eq!(
StaticTokenVerifier::new(maximum_entries)
.expect("the documented entry maximum is admissible")
.tokens
.len(),
MAX_STATIC_TOKEN_ENTRIES
);
let excessive_entries = (0..=MAX_STATIC_TOKEN_ENTRIES)
.map(|index| {
(
format!("token-{index}"),
AuthContext::with_subject(format!("owner-{index}")),
)
})
.collect::<Vec<_>>();
assert!(StaticTokenVerifier::new(excessive_entries).is_err());
let maximum_schemes = (0..MAX_ALLOWED_AUTH_SCHEMES)
.map(|index| format!("Scheme{index}"))
.collect::<Vec<_>>();
StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration")
.with_allowed_schemes(maximum_schemes)
.expect("the documented scheme maximum is admissible");
let excessive_schemes = (0..=MAX_ALLOWED_AUTH_SCHEMES)
.map(|index| format!("Scheme{index}"))
.collect::<Vec<_>>();
assert!(
StaticTokenVerifier::new([("token", AuthContext::with_subject("owner"))])
.expect("valid verifier configuration")
.with_allowed_schemes(excessive_schemes)
.is_err()
);
}
#[test]
fn token_auth_provider_with_scheme_restriction() {
let verifier = StaticTokenVerifier::new([("secret", AuthContext::with_subject("user"))])
.expect("valid verifier configuration")
.with_allowed_schemes(["Bearer"])
.expect("valid scheme configuration");
let provider = TokenAuthProvider::new(verifier);
let params = serde_json::json!({"authorization": "Basic secret"});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let err = provider.authenticate(&ctx(), req).unwrap_err();
assert!(err.message.contains("Unsupported"));
let params = serde_json::json!({"authorization": "Bearer secret"});
let req = AuthRequest {
method: "test",
params: Some(¶ms),
transport_authorization: None,
request_id: 1,
};
let auth = provider.authenticate(&ctx(), req).unwrap();
assert_eq!(auth.subject, Some("user".to_string()));
}
#[test]
fn auth_request_exposes_all_fields() {
let params = serde_json::json!({"key": "val"});
let req = AuthRequest {
method: "prompts/get",
params: Some(¶ms),
transport_authorization: None,
request_id: 99,
};
assert_eq!(req.method, "prompts/get");
assert_eq!(req.request_id, 99);
assert!(req.params.is_some());
}
}