use core::fmt;
pub use fastmcp_core::CanonicalHttpUrl;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BearerBindingError {
CleartextResource,
EmptyToken,
InvalidTokenBytes,
}
impl fmt::Display for BearerBindingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CleartextResource => {
formatter.write_str("bearer credentials bind only to https resources")
}
Self::EmptyToken => formatter.write_str("bearer token is empty"),
Self::InvalidTokenBytes => {
formatter.write_str("bearer token contains header-hostile bytes")
}
}
}
}
impl std::error::Error for BearerBindingError {}
#[derive(Clone)]
pub struct BoundBearerCredential {
resource: CanonicalHttpUrl,
token: String,
}
impl fmt::Debug for BoundBearerCredential {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BoundBearerCredential")
.field("resource", &self.resource.as_str())
.field("token", &"<redacted>")
.finish()
}
}
impl BoundBearerCredential {
pub fn bind(
resource: CanonicalHttpUrl,
token: impl Into<String>,
) -> Result<Self, BearerBindingError> {
if !resource.as_str().starts_with("https://") {
return Err(BearerBindingError::CleartextResource);
}
let token = token.into();
if token.is_empty() {
return Err(BearerBindingError::EmptyToken);
}
if token
.bytes()
.any(|byte| byte.is_ascii_control() || byte == b' ')
{
return Err(BearerBindingError::InvalidTokenBytes);
}
Ok(Self { resource, token })
}
#[must_use]
pub fn resource(&self) -> &CanonicalHttpUrl {
&self.resource
}
#[must_use]
pub fn authorization_for_target(&self, target: &CanonicalHttpUrl) -> Option<String> {
if target.as_str() == self.resource.as_str() {
Some(format!("Bearer {}", self.token))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::{BearerBindingError, BoundBearerCredential, CanonicalHttpUrl};
fn url(value: &str) -> CanonicalHttpUrl {
CanonicalHttpUrl::parse(value).expect("test URL is canonical")
}
#[test]
fn binds_only_to_https_resources() {
assert!(BoundBearerCredential::bind(url("https://mcp.example/api"), "token-1").is_ok());
for cleartext in [
"http://mcp.example/api",
"http://localhost:8080/api",
"http://127.0.0.1:8080/api",
"http://[::1]:8080/api",
] {
assert_eq!(
BoundBearerCredential::bind(url(cleartext), "token-1").err(),
Some(BearerBindingError::CleartextResource),
"cleartext resource {cleartext:?} must never hold a credential"
);
}
}
#[test]
fn refuses_empty_and_header_hostile_tokens() {
let resource = url("https://mcp.example/api");
assert_eq!(
BoundBearerCredential::bind(resource.clone(), "").err(),
Some(BearerBindingError::EmptyToken)
);
assert_eq!(
BoundBearerCredential::bind(resource.clone(), "to\r\nken").err(),
Some(BearerBindingError::InvalidTokenBytes)
);
assert_eq!(
BoundBearerCredential::bind(resource, "to ken").err(),
Some(BearerBindingError::InvalidTokenBytes)
);
}
#[test]
fn attaches_only_to_the_exact_bound_resource() {
let credential =
BoundBearerCredential::bind(url("https://mcp.example/api"), "token-1").expect("binds");
assert_eq!(
credential.authorization_for_target(&url("https://mcp.example/api")),
Some("Bearer token-1".to_owned())
);
for target in [
"https://mcp.example/other",
"https://other.example/api",
"http://mcp.example/api",
"https://mcp.example/api?extra=1",
] {
assert_eq!(
credential.authorization_for_target(&url(target)),
None,
"target {target:?} must not observe the token"
);
}
}
#[test]
fn debug_output_redacts_the_token() {
let credential =
BoundBearerCredential::bind(url("https://mcp.example/api"), "super-secret-token-value")
.expect("binds");
let debug = format!("{credential:?}");
assert!(debug.contains("<redacted>"));
assert!(
!debug.contains("super-secret-token-value"),
"the token must never appear in diagnostics: {debug}"
);
}
}