use std::sync::{Arc, Mutex};
use authplane_sdk::{AuthplaneResource, VerifiedClaims, VerifierError};
use fastmcp_rust::{
AccessToken, AuthContext, AuthRequest, McpContext, McpError, McpErrorCode, McpResult,
TokenVerifier,
};
use serde_json::json;
pub struct AuthplaneFastMcpTokenVerifier {
verifier: Arc<AuthplaneResource>,
runtime: Mutex<tokio::runtime::Runtime>,
}
impl AuthplaneFastMcpTokenVerifier {
pub fn new(verifier: Arc<AuthplaneResource>) -> McpResult<Self> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| {
McpError::tool_error(format!(
"failed to build authplane verifier runtime: {error}"
))
})?;
Ok(Self {
verifier,
runtime: Mutex::new(runtime),
})
}
}
impl std::fmt::Debug for AuthplaneFastMcpTokenVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthplaneFastMcpTokenVerifier")
.finish_non_exhaustive()
}
}
impl TokenVerifier for AuthplaneFastMcpTokenVerifier {
fn verify(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
token: &AccessToken,
) -> McpResult<AuthContext> {
if !token.scheme.eq_ignore_ascii_case("Bearer") {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
format!(
"Unsupported access token scheme {scheme:?}; \
fastmcp-rust 0.3 adapter only honors Bearer",
scheme = token.scheme
),
));
}
let runtime = self
.runtime
.lock()
.map_err(|_| McpError::tool_error("authplane verifier runtime mutex poisoned"))?;
let token_str = token.token.clone();
let verifier = self.verifier.clone();
let claims = runtime
.block_on(async move { verifier.verify(&token_str).await })
.map_err(|error| {
verifier_error_to_mcp_error(&error, self.verifier.resource_metadata_url())
})?;
Ok(claims_to_auth_context(&claims, token.clone()))
}
}
fn claims_to_auth_context(claims: &VerifiedClaims, token: AccessToken) -> AuthContext {
AuthContext {
subject: Some(claims.sub.clone()),
scopes: claims.scopes.clone(),
token: Some(token),
claims: Some(json!({
"iss": claims.issuer,
"sub": claims.sub,
"aud": claims.audience,
"scope": claims.scopes.join(" "),
"jti": claims.jti,
"exp": claims.expires_at,
"iat": claims.issued_at,
"nbf": claims.not_before,
})),
}
}
fn verifier_error_to_mcp_error(error: &VerifierError, resource_metadata_url: &str) -> McpError {
let code = match error {
VerifierError::InsufficientScope { .. } => McpErrorCode::ResourceForbidden,
VerifierError::MetadataUnavailable { .. } | VerifierError::JwksUnavailable { .. } => {
McpErrorCode::InternalError
}
_ => McpErrorCode::ResourceForbidden,
};
McpError::with_data(
code,
error.to_string(),
json!({ "resource_metadata": resource_metadata_url }),
)
}
#[cfg(test)]
mod tests {
use super::*;
const PRM_URL: &str = "https://api.example.com/.well-known/oauth-protected-resource/mcp";
#[test]
fn rejected_token_carries_resource_metadata_in_error_data() {
let error = verifier_error_to_mcp_error(&VerifierError::TokenExpired, PRM_URL);
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
assert_eq!(error.data, Some(json!({ "resource_metadata": PRM_URL })));
assert_eq!(error.masked(true).data, error.data);
}
#[test]
fn service_side_failure_maps_to_internal_error() {
let error = verifier_error_to_mcp_error(
&VerifierError::JwksUnavailable {
message: "down".to_string(),
},
PRM_URL,
);
assert_eq!(error.code, McpErrorCode::InternalError);
}
}