mcp: add bearer token verification seam

This commit is contained in:
a.tolmachev
2026-05-03 18:43:48 +00:00
parent ffcce360fc
commit 46f8e6cbeb
5 changed files with 178 additions and 13 deletions
+46 -11
View File
@@ -41,6 +41,7 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use tracing::info;
use crate::{
auth::{SharedMachineCredentialVerifier, VerifiedMachineCredential},
catalog::PublishedToolCatalog,
jsonrpc::{
CURRENT_PROTOCOL_VERSION, DEFAULT_PROTOCOL_VERSION, is_notification, is_request,
@@ -70,6 +71,7 @@ pub struct AppState {
api_rate_limiter: RequestRateLimiter,
secret_crypto: SecretCrypto,
sessions: SharedSessionStore,
credential_verifier: SharedMachineCredentialVerifier,
allowed_origins: AllowedOrigins,
}
@@ -133,6 +135,7 @@ pub fn build_app(
runtime: RuntimeExecutor,
api_rate_limiter: RequestRateLimiter,
sessions: SharedSessionStore,
credential_verifier: SharedMachineCredentialVerifier,
) -> Router {
let state = Arc::new(AppState {
registry: registry.clone(),
@@ -141,6 +144,7 @@ pub fn build_app(
api_rate_limiter,
secret_crypto,
sessions,
credential_verifier,
allowed_origins: AllowedOrigins::new(public_base_url),
});
@@ -174,7 +178,7 @@ async fn mcp_get(
}
if let Err(status) =
require_platform_api_key(&state, &path, &headers, PlatformApiKeyScope::Read).await
require_machine_access(&state, &path, &headers, PlatformApiKeyScope::Read).await
{
return status.into_response();
}
@@ -222,7 +226,7 @@ async fn mcp_delete(
headers: HeaderMap,
) -> Response {
if let Err(status) =
require_platform_api_key(&state, &path, &headers, PlatformApiKeyScope::Read).await
require_machine_access(&state, &path, &headers, PlatformApiKeyScope::Read).await
{
return status.into_response();
}
@@ -321,7 +325,7 @@ async fn mcp_post(
Some("tools/call") => PlatformApiKeyScope::Write,
_ => PlatformApiKeyScope::Read,
};
if let Err(status) = require_platform_api_key(&state, &path, &headers, required_scope).await {
if let Err(status) = require_machine_access(&state, &path, &headers, required_scope).await {
return with_request_id_header(status.into_response(), &transport_request_id);
}
@@ -1803,13 +1807,44 @@ async fn require_initialized_session(
Ok(session)
}
async fn require_platform_api_key(
async fn require_machine_access(
state: &Arc<AppState>,
path: &AgentRoutePath,
headers: &HeaderMap,
required_scope: PlatformApiKeyScope,
) -> Result<(), StatusCode> {
) -> Result<VerifiedMachineCredential, StatusCode> {
let secret = bearer_token(headers).ok_or(StatusCode::UNAUTHORIZED)?;
let credential = resolve_machine_credential(state, path, secret).await?;
if !allows_scope(&credential.scopes, required_scope) {
return Err(StatusCode::FORBIDDEN);
}
Ok(credential)
}
async fn resolve_machine_credential(
state: &Arc<AppState>,
path: &AgentRoutePath,
token: &str,
) -> Result<VerifiedMachineCredential, StatusCode> {
if let Some(credential) = verify_static_agent_key(state, path, token).await? {
return Ok(credential);
}
state
.credential_verifier
.verify_bearer_token(&path.workspace_slug, &path.agent_slug, token)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)
}
async fn verify_static_agent_key(
state: &Arc<AppState>,
path: &AgentRoutePath,
secret: &str,
) -> Result<Option<VerifiedMachineCredential>, StatusCode> {
let secret_hash = hash_access_secret(secret);
let Some(api_key) = state
.registry
@@ -1821,13 +1856,9 @@ async fn require_platform_api_key(
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
else {
return Err(StatusCode::UNAUTHORIZED);
return Ok(None);
};
if !allows_scope(&api_key.api_key.scopes, required_scope) {
return Err(StatusCode::FORBIDDEN);
}
let used_at = OffsetDateTime::now_utc();
state
.registry
@@ -1835,7 +1866,11 @@ async fn require_platform_api_key(
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(())
Ok(Some(VerifiedMachineCredential {
machine_access_mode: crank_core::MachineAccessMode::StaticAgentKey,
max_security_level: crank_core::OperationSecurityLevel::Standard,
scopes: api_key.api_key.scopes,
}))
}
fn bearer_token(headers: &HeaderMap) -> Option<&str> {