use std::collections::HashMap;
use serde_json::{Value, json};
use turbomcp_protocol::{Error as McpError, Result as McpResult};
use crate::config::{BearerTokenMethod, ProtectedResourceMetadata};
#[derive(Debug, Clone)]
pub struct ProtectedResourceMetadataBuilder {
base_resource_uri: String,
auth_servers: Vec<String>,
scopes: Vec<String>,
bearer_methods: Vec<BearerTokenMethod>,
documentation_uri: Option<String>,
}
impl ProtectedResourceMetadataBuilder {
pub fn new(base_resource_uri: String, auth_server: String) -> Self {
Self {
base_resource_uri,
auth_servers: vec![auth_server],
scopes: vec!["openid".to_string(), "profile".to_string()],
bearer_methods: vec![BearerTokenMethod::Header, BearerTokenMethod::Body],
documentation_uri: None,
}
}
pub fn with_additional_authorization_server(mut self, auth_server: String) -> Self {
self.auth_servers.push(auth_server);
self
}
pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
self.scopes = scopes;
self
}
pub fn with_bearer_methods(mut self, methods: Vec<BearerTokenMethod>) -> Self {
self.bearer_methods = methods;
self
}
pub fn with_documentation(mut self, uri: String) -> Self {
self.documentation_uri = Some(uri);
self
}
pub fn build(self) -> Value {
let mut metadata = json!({
"resource": self.base_resource_uri,
"authorization_servers": self.auth_servers,
"scopes_supported": self.scopes,
"bearer_methods_supported": self.bearer_methods
.iter()
.map(|m| match m {
BearerTokenMethod::Header => "header",
BearerTokenMethod::Query => "query",
BearerTokenMethod::Body => "body",
})
.collect::<Vec<_>>(),
});
if let Some(doc) = self.documentation_uri {
metadata["resource_documentation"] = Value::String(doc);
}
metadata
}
pub fn build_struct(self) -> ProtectedResourceMetadata {
ProtectedResourceMetadata {
resource: self.base_resource_uri,
authorization_servers: self.auth_servers,
scopes_supported: Some(self.scopes),
bearer_methods_supported: Some(self.bearer_methods),
resource_documentation: self.documentation_uri,
additional_metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct WwwAuthenticateBuilder {
metadata_uri: String,
scope: Option<String>,
error: Option<String>,
error_description: Option<String>,
}
impl WwwAuthenticateBuilder {
pub fn new(metadata_uri: String) -> Self {
Self {
metadata_uri,
scope: None,
error: None,
error_description: None,
}
}
pub fn invalid_token(metadata_uri: String, description: Option<String>) -> Self {
Self::new(metadata_uri).with_error("invalid_token".to_string(), description)
}
pub fn insufficient_scope(
metadata_uri: String,
scope: String,
description: Option<String>,
) -> Self {
Self::new(metadata_uri)
.with_scope(scope)
.with_error("insufficient_scope".to_string(), description)
}
pub fn with_scope(mut self, scope: String) -> Self {
self.scope = Some(scope);
self
}
pub fn with_error(mut self, error: String, description: Option<String>) -> Self {
self.error = Some(error);
self.error_description = description;
self
}
pub fn build(self) -> String {
let mut parts = vec![format!(
"Bearer resource_metadata=\"{}\"",
self.metadata_uri
)];
if let Some(scope) = self.scope {
parts.push(format!("scope=\"{}\"", scope));
}
if let Some(error) = self.error {
parts.push(format!("error=\"{}\"", error));
}
if let Some(description) = self.error_description {
parts.push(format!("error_description=\"{}\"", description));
}
parts.join(", ")
}
}
#[derive(Debug, Clone)]
pub struct BearerTokenValidator;
impl BearerTokenValidator {
pub fn extract_from_header(authorization_header: &str) -> McpResult<String> {
let parts: Vec<&str> = authorization_header.split_whitespace().collect();
if parts.len() != 2 {
return Err(McpError::invalid_params(
"Authorization header must have format: Bearer <token>".to_string(),
));
}
if parts[0].to_lowercase() != "bearer" {
return Err(McpError::invalid_params(
"Only Bearer token authentication is supported".to_string(),
));
}
Ok(parts[1].to_string())
}
pub fn validate_format(token: &str) -> McpResult<()> {
if token.is_empty() {
return Err(McpError::invalid_params("Token is empty".to_string()));
}
if token.len() < 10 {
return Err(McpError::invalid_params("Token is too short".to_string()));
}
if token.len() > 10000 {
return Err(McpError::invalid_params("Token is too long".to_string()));
}
Ok(())
}
}
#[cfg(feature = "mcp-http-server")]
pub struct JwtBearerValidator {
validator: crate::jwt::JwtValidator,
required_scopes: Vec<String>,
}
#[cfg(feature = "mcp-http-server")]
impl JwtBearerValidator {
pub fn new(validator: crate::jwt::JwtValidator) -> Self {
Self {
validator,
required_scopes: Vec::new(),
}
}
#[must_use]
pub fn with_required_scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.required_scopes = scopes.into_iter().map(Into::into).collect();
self
}
}
#[cfg(feature = "mcp-http-server")]
impl turbomcp_server::BearerTokenValidator for JwtBearerValidator {
fn validate<'a>(&'a self, token: &'a str) -> turbomcp_server::ValidationFuture<'a> {
Box::pin(async move {
let required: Vec<&str> = self.required_scopes.iter().map(String::as_str).collect();
match validate_bearer_token(&self.validator, token, &required).await {
Ok(context) => {
let mut principal =
turbomcp_protocol::mcp_core::auth::Principal::new(context.sub)
.with_roles(context.roles);
principal.issuer = context.iss;
principal.audience = context.aud;
principal.expires_at = context.exp;
principal.email = context.user.email;
principal.name = context.user.display_name;
if !context.scopes.is_empty() {
principal =
principal.with_claim("scope", Value::String(context.scopes.join(" ")));
}
Ok(principal)
}
Err(TokenValidationError::InvalidToken(error)) => Err(
turbomcp_server::BearerRejection::InvalidToken(error.to_string()),
),
Err(TokenValidationError::InsufficientScope { required, .. }) => {
Err(turbomcp_server::BearerRejection::InsufficientScope { required })
}
}
})
}
}
#[derive(Debug)]
pub enum TokenValidationError {
InvalidToken(McpError),
InsufficientScope {
required: Vec<String>,
granted: Vec<String>,
},
}
impl std::fmt::Display for TokenValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidToken(e) => write!(f, "invalid_token: {e}"),
Self::InsufficientScope { required, granted } => write!(
f,
"insufficient_scope: requires [{}], token has [{}]",
required.join(", "),
granted.join(", ")
),
}
}
}
impl std::error::Error for TokenValidationError {}
pub async fn validate_bearer_token(
validator: &crate::jwt::JwtValidator,
token: &str,
required_scopes: &[&str],
) -> Result<crate::context::AuthContext, TokenValidationError> {
let result = validator
.validate_with_refresh(token)
.await
.map_err(TokenValidationError::InvalidToken)?;
let claims = result.claims;
let granted: Vec<String> = claims
.additional
.get("scope")
.and_then(Value::as_str)
.map(|s| s.split_whitespace().map(str::to_string).collect())
.unwrap_or_default();
let string_claim = |name: &str| {
claims
.additional
.get(name)
.and_then(Value::as_str)
.map(str::to_string)
};
let roles: Vec<String> = claims
.additional
.get("roles")
.and_then(Value::as_array)
.map(|roles| {
roles
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
let subject = claims.sub.clone().unwrap_or_default();
let mut builder = crate::context::AuthContext::builder()
.subject(subject.clone())
.user(crate::types::UserInfo {
id: subject.clone(),
username: subject,
email: string_claim("email"),
display_name: string_claim("name"),
avatar_url: None,
metadata: HashMap::new(),
})
.provider("jwt")
.roles(roles)
.scopes(granted.clone())
.authenticated_at(std::time::SystemTime::now());
if let Some(iss) = claims.iss {
builder = builder.iss(iss);
}
if let Some(aud) = claims.aud.and_then(|values| values.into_iter().next()) {
builder = builder.aud(aud);
}
if let Some(exp) = claims.exp {
builder = builder.exp(exp);
}
if let Some(jti) = claims.jti {
builder = builder.jti(jti);
}
let auth_context = builder
.build()
.map_err(|e| TokenValidationError::InvalidToken(McpError::internal(e.to_string())))?;
if !required_scopes.is_empty() && !auth_context.has_all_scopes(required_scopes) {
return Err(TokenValidationError::InsufficientScope {
required: required_scopes.iter().map(|s| s.to_string()).collect(),
granted,
});
}
Ok(auth_context)
}
pub fn unauthorized_response_body(metadata_uri: &str, scope: Option<&str>) -> Value {
let mut response = json!({
"error": "unauthorized",
"error_description": "Valid bearer token required",
"metadata_uri": metadata_uri,
});
if let Some(s) = scope {
response["required_scope"] = Value::String(s.to_string());
}
response
}
pub fn validate_audience(token_aud: &str, server_uri: &str) -> turbomcp_protocol::Result<()> {
use url::Url;
let token_url = Url::parse(token_aud).map_err(|e| {
turbomcp_protocol::Error::invalid_params(format!("Invalid token audience URI: {}", e))
})?;
let server_url = Url::parse(server_uri).map_err(|e| {
turbomcp_protocol::Error::invalid_params(format!("Invalid server URI: {}", e))
})?;
let token_normalized = normalize_resource_uri(&token_url);
let server_normalized = normalize_resource_uri(&server_url);
let matches: bool =
subtle::ConstantTimeEq::ct_eq(token_normalized.as_bytes(), server_normalized.as_bytes())
.into();
if !matches {
return Err(turbomcp_protocol::Error::invalid_params(format!(
"Token audience '{}' does not match server URI '{}' (normalized: '{}' vs '{}')",
token_aud, server_uri, token_normalized, server_normalized
)));
}
Ok(())
}
fn normalize_resource_uri(url: &url::Url) -> String {
let mut normalized = String::new();
normalized.push_str(&url.scheme().to_lowercase());
normalized.push_str("://");
if let Some(host) = url.host_str() {
normalized.push_str(&host.to_lowercase());
}
if let Some(port) = url.port() {
let default_port = match url.scheme() {
"http" => 80,
"https" => 443,
_ => 0,
};
if port != default_port {
normalized.push(':');
normalized.push_str(&port.to_string());
}
}
let path = url.path();
if path != "/" {
normalized.push_str(path.trim_end_matches('/'));
}
normalized
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_builder() {
let metadata = ProtectedResourceMetadataBuilder::new(
"https://api.example.com".to_string(),
"https://auth.example.com".to_string(),
)
.with_scopes(vec!["openid".to_string(), "profile".to_string()])
.with_documentation("https://api.example.com/docs".to_string())
.build();
assert_eq!(metadata["resource"], "https://api.example.com");
assert_eq!(
metadata["authorization_servers"],
serde_json::json!(["https://auth.example.com"])
);
}
#[test]
fn test_metadata_builder_multiple_authorization_servers() {
let metadata = ProtectedResourceMetadataBuilder::new(
"https://api.example.com".to_string(),
"https://auth-primary.example.com".to_string(),
)
.with_additional_authorization_server("https://auth-secondary.example.com".to_string())
.build();
assert_eq!(
metadata["authorization_servers"],
serde_json::json!([
"https://auth-primary.example.com",
"https://auth-secondary.example.com"
])
);
}
#[test]
fn test_www_authenticate_builder() {
let header = WwwAuthenticateBuilder::new(
"https://api.example.com/.well-known/protected-resource".to_string(),
)
.with_scope("openid profile".to_string())
.build();
assert!(header.contains("Bearer"));
assert!(header.contains("resource_metadata"));
assert!(header.contains("scope"));
}
#[test]
fn test_www_authenticate_invalid_token() {
let header = WwwAuthenticateBuilder::invalid_token(
"https://api.example.com/.well-known/oauth-protected-resource".to_string(),
Some("Token expired".to_string()),
)
.build();
assert!(header.contains("error=\"invalid_token\""));
assert!(header.contains("error_description=\"Token expired\""));
assert!(header.contains("resource_metadata"));
}
#[test]
fn test_www_authenticate_insufficient_scope() {
let header = WwwAuthenticateBuilder::insufficient_scope(
"https://api.example.com/.well-known/oauth-protected-resource".to_string(),
"mcp:tools:write".to_string(),
None,
)
.build();
assert!(header.contains("error=\"insufficient_scope\""));
assert!(header.contains("scope=\"mcp:tools:write\""));
}
#[test]
fn test_bearer_token_extraction() {
let token = BearerTokenValidator::extract_from_header("Bearer mytoken123")
.expect("Failed to extract token");
assert_eq!(token, "mytoken123");
}
#[test]
fn test_bearer_token_extraction_case_insensitive() {
let token = BearerTokenValidator::extract_from_header("bearer mytoken123")
.expect("Failed to extract token");
assert_eq!(token, "mytoken123");
}
#[test]
fn test_bearer_token_extraction_invalid_format() {
let result = BearerTokenValidator::extract_from_header("mytoken123");
assert!(result.is_err());
}
#[test]
fn test_unauthorized_response() {
let response = unauthorized_response_body(
"https://api.example.com/.well-known/protected-resource",
Some("openid"),
);
assert_eq!(response["error"], "unauthorized");
assert!(response.get("metadata_uri").is_some());
}
#[test]
fn test_audience_validation_exact_match() {
assert!(validate_audience("https://api.example.com", "https://api.example.com").is_ok());
}
#[test]
fn test_audience_validation_trailing_slash() {
assert!(validate_audience("https://api.example.com/", "https://api.example.com").is_ok());
assert!(validate_audience("https://api.example.com", "https://api.example.com/").is_ok());
}
#[test]
fn test_audience_validation_case_insensitive() {
assert!(validate_audience("https://API.EXAMPLE.COM", "https://api.example.com").is_ok());
assert!(validate_audience("HTTPS://api.example.com", "https://api.example.com").is_ok());
}
#[test]
fn test_audience_validation_port_mismatch() {
assert!(
validate_audience("https://api.example.com:8080", "https://api.example.com").is_err()
);
}
#[test]
fn test_audience_validation_path_significant() {
assert!(
validate_audience("https://api.example.com/mcp", "https://api.example.com").is_err()
);
assert!(
validate_audience("https://api.example.com", "https://api.example.com/mcp").is_err()
);
}
#[test]
fn test_audience_validation_default_ports() {
assert!(
validate_audience("https://api.example.com:443", "https://api.example.com").is_ok()
);
assert!(validate_audience("http://api.example.com:80", "http://api.example.com").is_ok());
}
#[test]
fn test_normalize_resource_uri() {
use url::Url;
let url = Url::parse("https://API.EXAMPLE.COM:443/path/").unwrap();
assert_eq!(normalize_resource_uri(&url), "https://api.example.com/path");
let url = Url::parse("http://example.com:80").unwrap();
assert_eq!(normalize_resource_uri(&url), "http://example.com");
let url = Url::parse("https://example.com:8443/").unwrap();
assert_eq!(normalize_resource_uri(&url), "https://example.com:8443");
}
}