pub(crate) fn bearer_ok(expected: &str, header: Option<&str>) -> bool {
let presented = match header.and_then(|h| h.strip_prefix("Bearer ")) {
Some(t) => t,
None => return false,
};
let a = expected.as_bytes();
let b = presented.as_bytes();
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
use crate::mcp::AingleMcp;
use crate::state::AppState;
use axum::response::IntoResponse;
use axum::Router;
#[cfg(feature = "mcp-oauth")]
fn metadata_url_from_resource(resource: &str) -> Option<String> {
let rest = resource.split("://").nth(1)?; let authority = rest.split('/').next()?; let scheme = resource.split("://").next()?; if authority.is_empty() {
return None;
}
Some(format!(
"{scheme}://{authority}/.well-known/oauth-protected-resource"
))
}
pub fn mcp_http_router(
state: AppState,
token: Option<String>,
allow_anonymous: bool,
public_hosts: Vec<String>,
#[cfg(feature = "mcp-oauth")] oauth: Option<(
crate::mcp::oauth::OAuthConfig,
crate::mcp::oauth::JwksCache,
)>,
) -> Option<Router> {
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
};
use std::sync::Arc;
#[cfg(feature = "mcp-oauth")]
let oauth_enabled = oauth.is_some();
#[cfg(not(feature = "mcp-oauth"))]
let oauth_enabled = false;
if token.is_none() && !allow_anonymous && !oauth_enabled {
return None;
}
state.set_mcp_token(token.clone());
let auth_state = state.clone();
let factory_state = state;
let mut allowed_hosts = vec![
"localhost".to_string(),
"127.0.0.1".to_string(),
"::1".to_string(),
];
allowed_hosts.extend(public_hosts.clone());
let config = StreamableHttpServerConfig::default()
.with_allowed_hosts(allowed_hosts)
.with_stateful_mode(false)
.with_json_response(true);
let service = StreamableHttpService::new(
move || Ok(AingleMcp::new(factory_state.clone())),
Arc::new(LocalSessionManager::default()),
config,
);
let mut router = Router::new().fallback_service(service);
if !allow_anonymous {
#[cfg(feature = "mcp-oauth")]
let oauth_for_layer = oauth.clone();
let resource_metadata_url = {
#[cfg(feature = "mcp-oauth")]
let from_oauth = oauth
.as_ref()
.and_then(|(cfg, _)| metadata_url_from_resource(&cfg.resource));
#[cfg(not(feature = "mcp-oauth"))]
let from_oauth: Option<String> = None;
from_oauth
.or_else(|| {
public_hosts
.first()
.map(|h| format!("https://{h}/.well-known/oauth-protected-resource"))
})
.unwrap_or_default()
};
router = router.layer(axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let auth_state = auth_state.clone();
#[cfg(feature = "mcp-oauth")]
let oauth_for_layer = oauth_for_layer.clone();
let rmu = resource_metadata_url.clone();
async move {
let hdr = req
.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok());
for t in auth_state.mcp_tokens_snapshot() {
if bearer_ok(&t, hdr) {
return next.run(req).await;
}
}
#[cfg(feature = "mcp-oauth")]
if let Some((ref cfg, ref jwks)) = oauth_for_layer {
if let Some(raw) = hdr.and_then(|h| h.strip_prefix("Bearer ")) {
if let Some(kid) = crate::mcp::oauth::token_kid(raw) {
if let Some(key) = jwks.key_for(&kid).await {
if crate::mcp::oauth::validate_jwt(raw, &key, cfg).is_ok() {
return next.run(req).await;
}
}
}
}
}
let www = if rmu.is_empty() {
"Bearer".to_string()
} else {
format!("Bearer resource_metadata=\"{rmu}\", scope=\"mcp\"")
};
(
axum::http::StatusCode::UNAUTHORIZED,
[(axum::http::header::WWW_AUTHENTICATE, www)],
"unauthorized",
)
.into_response()
}
},
));
}
Some(router)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bearer_check() {
assert!(bearer_ok("secret", Some("Bearer secret")));
assert!(!bearer_ok("secret", Some("Bearer wrong")));
assert!(!bearer_ok("secret", Some("secret"))); assert!(!bearer_ok("secret", None)); assert!(!bearer_ok("secret", Some("Bearer sec"))); }
#[tokio::test]
async fn token_rotation_is_live() {
use axum::body::Body;
use axum::http::{header::AUTHORIZATION, Request, StatusCode};
use tower::ServiceExt;
let state = AppState::new().expect("in-memory state");
let router = mcp_http_router(
state.clone(),
Some("tok-a".to_string()),
false,
vec![],
#[cfg(feature = "mcp-oauth")]
None,
)
.expect("router builds when a token is configured");
async fn status_for(router: &Router, bearer: &str) -> StatusCode {
let req = Request::builder()
.uri("/")
.header(AUTHORIZATION, format!("Bearer {bearer}"))
.body(Body::empty())
.unwrap();
router.clone().oneshot(req).await.unwrap().status()
}
assert_ne!(status_for(&router, "tok-a").await, StatusCode::UNAUTHORIZED);
assert!(status_for(&router, "tok-a").await.as_u16() < 500);
assert_eq!(status_for(&router, "tok-b").await, StatusCode::UNAUTHORIZED);
state.set_mcp_token(Some("tok-b".to_string()));
assert_eq!(status_for(&router, "tok-a").await, StatusCode::UNAUTHORIZED);
assert_ne!(status_for(&router, "tok-b").await, StatusCode::UNAUTHORIZED);
assert!(status_for(&router, "tok-b").await.as_u16() < 500);
state.set_mcp_tokens(vec!["tok-c".to_string(), "tok-d".to_string()]);
assert_ne!(status_for(&router, "tok-c").await, StatusCode::UNAUTHORIZED);
assert_ne!(status_for(&router, "tok-d").await, StatusCode::UNAUTHORIZED);
assert_eq!(status_for(&router, "tok-b").await, StatusCode::UNAUTHORIZED);
state.set_mcp_tokens(vec!["tok-d".to_string()]);
assert_eq!(status_for(&router, "tok-c").await, StatusCode::UNAUTHORIZED);
assert_ne!(status_for(&router, "tok-d").await, StatusCode::UNAUTHORIZED);
state.set_mcp_tokens(Vec::new());
assert_eq!(status_for(&router, "tok-d").await, StatusCode::UNAUTHORIZED);
}
}