Skip to content

Commit 8ed361b

Browse files
test(conformance): cover EMA refresh-token exchange
1 parent dc26f27 commit 8ed361b

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

‎conformance/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ rmcp = { path = "../crates/rmcp", features = [
1919
"elicitation",
2020
"auth",
2121
"auth-client-credentials-jwt",
22+
"auth-enterprise-managed",
2223
"request-state",
2324
"transport-streamable-http-server",
2425
"transport-streamable-http-client-reqwest",
@@ -31,6 +32,7 @@ tracing = "0.1"
3132
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3233
axum = { version = "0.8", features = ["macros"] }
3334
anyhow = "1"
35+
oauth2 = { version = "5.0", default-features = false }
3436
reqwest = { version = "0.13", features = ["json"] }
3537
urlencoding = "2"
3638
url = "2"

‎conformance/src/bin/client.rs‎

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use anyhow::Context;
2+
use oauth2::{ClientSecret, RefreshToken};
13
use rmcp::{
24
ClientHandler, ClientLifecycleMode, ClientServiceExt, ErrorData, RoleClient, ServiceExt,
35
model::*,
@@ -6,7 +8,8 @@ use rmcp::{
68
AuthClient, AuthorizationManager, StreamableHttpClientTransport,
79
auth::{
810
AuthorizationCallback, AuthorizationRequest, ClientCredentialsConfig,
9-
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState,
11+
InMemoryCredentialStore, JwtSigningAlgorithm, OAuthState, default_oauth_http_client,
12+
enterprise::{EmaAuthorizationServer, EmaClientAuthentication, EmaExchangeRequest},
1013
},
1114
streamable_http_client::StreamableHttpClientTransportConfig,
1215
},
@@ -36,6 +39,17 @@ struct ConformanceContext {
3639
private_key_pem: Option<String>,
3740
#[serde(default)]
3841
signing_algorithm: Option<String>,
42+
// enterprise-managed-authorization-refresh-token
43+
#[serde(default)]
44+
idp_client_id: Option<String>,
45+
#[serde(default)]
46+
idp_client_secret: Option<String>,
47+
#[serde(default)]
48+
idp_refresh_token: Option<String>,
49+
#[serde(default)]
50+
idp_issuer: Option<String>,
51+
#[serde(default)]
52+
idp_token_endpoint: Option<String>,
3953
}
4054

4155
fn load_context() -> ConformanceContext {
@@ -760,6 +774,66 @@ async fn run_client_credentials_jwt(
760774
Ok(())
761775
}
762776

777+
/// Exchange the fixture's IdP refresh token, then exercise authenticated MCP access.
778+
async fn run_ema_refresh_token_client(
779+
server_url: &str,
780+
ctx: &ConformanceContext,
781+
) -> anyhow::Result<()> {
782+
let manager = AuthorizationManager::new(server_url).await?;
783+
let metadata = manager.resolve_metadata().await?.metadata;
784+
let idp = EmaAuthorizationServer::new(
785+
ctx.idp_issuer.as_deref().context("Missing idp_issuer")?,
786+
ctx.idp_token_endpoint
787+
.as_deref()
788+
.context("Missing idp_token_endpoint")?,
789+
ctx.idp_client_id
790+
.as_deref()
791+
.context("Missing idp_client_id")?,
792+
)
793+
.with_client_authentication(EmaClientAuthentication::ClientSecretBasic(
794+
ClientSecret::new(
795+
ctx.idp_client_secret
796+
.clone()
797+
.context("Missing idp_client_secret")?,
798+
),
799+
));
800+
let resource_as = EmaAuthorizationServer::new(
801+
metadata
802+
.issuer
803+
.context("Missing authorization server issuer")?,
804+
metadata.token_endpoint,
805+
ctx.client_id.as_deref().context("Missing client_id")?,
806+
)
807+
.with_client_authentication(EmaClientAuthentication::ClientSecretBasic(
808+
ClientSecret::new(ctx.client_secret.clone().context("Missing client_secret")?),
809+
));
810+
let refresh_token = RefreshToken::new(
811+
ctx.idp_refresh_token
812+
.clone()
813+
.context("Missing idp_refresh_token")?,
814+
);
815+
let http = default_oauth_http_client()?;
816+
let token = EmaExchangeRequest::new(idp, resource_as, server_url, &refresh_token)
817+
.with_scopes(manager.select_scopes(None, &[]))
818+
.exchange(&http, &http)
819+
.await?;
820+
821+
let transport = StreamableHttpClientTransport::from_config(
822+
StreamableHttpClientTransportConfig::with_uri(server_url)
823+
.auth_header(token.access_token.secret()),
824+
);
825+
let client = BasicClientHandler
826+
.serve_with_lifecycle(transport, conformance_lifecycle())
827+
.await?;
828+
let tools = client.list_tools(Default::default()).await?;
829+
for tool in tools.tools {
830+
let args = build_tool_arguments(&tool);
831+
client.call_tool(call_tool_params(tool.name, args)).await?;
832+
}
833+
client.cancel().await?;
834+
Ok(())
835+
}
836+
763837
/// Cross-app access flow (SEP-1046 extension).
764838
async fn run_cross_app_access_client(
765839
server_url: &str,
@@ -1110,6 +1184,11 @@ async fn run_scenario(
11101184
"auth/client-credentials-basic" => run_client_credentials_basic(server_url, ctx).await?,
11111185
"auth/client-credentials-jwt" => run_client_credentials_jwt(server_url, ctx).await?,
11121186

1187+
// Auth - enterprise-managed authorization with a refresh-token subject
1188+
"auth/enterprise-managed-authorization-refresh-token" => {
1189+
run_ema_refresh_token_client(server_url, ctx).await?
1190+
}
1191+
11131192
// Auth - cross-app access
11141193
"auth/cross-app-access-complete-flow" => {
11151194
run_cross_app_access_client(server_url, ctx).await?

0 commit comments

Comments
 (0)