use async_trait::async_trait;
use rust_mcp_sdk::{
auth::{AuthInfo, AuthProvider, AuthenticationError, OauthEndpoint},
mcp_http::{self, GenericBody, GenericBodyExt, McpAppState},
mcp_server::error::TransportServerError,
};
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, SystemTime},
};
pub struct ConfigTokenAuthProvider {
token: String,
}
impl ConfigTokenAuthProvider {
pub fn new(token: impl Into<String>) -> Self {
Self {
token: token.into(),
}
}
}
const TOKEN_TTL: Duration = Duration::from_secs(365 * 24 * 3600);
#[async_trait]
impl AuthProvider for ConfigTokenAuthProvider {
async fn verify_token(&self, access_token: String) -> Result<AuthInfo, AuthenticationError> {
if access_token == self.token {
Ok(AuthInfo {
token_unique_id: access_token,
client_id: None,
user_id: None,
scopes: None,
expires_at: Some(SystemTime::now() + TOKEN_TTL),
audience: None,
extra: None,
})
} else {
Err(AuthenticationError::InvalidOrExpiredToken(
"invalid token".into(),
))
}
}
fn auth_endpoints(&self) -> Option<&HashMap<String, OauthEndpoint>> {
None
}
fn protected_resource_metadata_url(&self) -> Option<&str> {
None
}
async fn handle_request(
&self,
_request: mcp_http::http::Request<&str>,
_state: Arc<McpAppState>,
) -> Result<mcp_http::http::Response<GenericBody>, TransportServerError> {
Ok(GenericBody::create_404_response())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn provider(token: &str) -> ConfigTokenAuthProvider {
ConfigTokenAuthProvider::new(token)
}
#[tokio::test]
async fn valid_token_returns_auth_info() {
let p = provider("secret");
let info = p.verify_token("secret".into()).await.unwrap();
assert_eq!(info.token_unique_id, "secret");
assert!(info.expires_at.is_some());
assert!(info.expires_at.unwrap() > SystemTime::now());
}
#[tokio::test]
async fn invalid_token_returns_error() {
let p = provider("secret");
let err = p.verify_token("wrong".into()).await.unwrap_err();
assert!(
matches!(err, AuthenticationError::InvalidOrExpiredToken(_)),
"expected InvalidOrExpiredToken, got {err:?}"
);
}
#[tokio::test]
async fn empty_token_is_rejected() {
let p = provider("secret");
assert!(p.verify_token(String::new()).await.is_err());
}
#[tokio::test]
async fn empty_configured_token_accepts_empty_bearer() {
let p = provider("");
assert!(p.verify_token(String::new()).await.is_ok());
}
#[test]
fn auth_endpoints_is_none() {
assert!(provider("t").auth_endpoints().is_none());
}
#[test]
fn protected_resource_metadata_url_is_none() {
assert!(provider("t").protected_resource_metadata_url().is_none());
}
#[test]
fn required_scopes_is_none() {
assert!(provider("t").required_scopes().is_none());
}
}