Skip to content

Commit b387a7a

Browse files
committed
fix(auth): discover path-appended OAuth metadata
1 parent f1ef2ec commit b387a7a

2 files changed

Lines changed: 73 additions & 13 deletions

File tree

crates/rmcp/src/transport/auth.rs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,14 +1409,16 @@ impl AuthorizationManager {
14091409
push_candidate("/.well-known/oauth-authorization-server".to_string());
14101410
push_candidate("/.well-known/openid-configuration".to_string());
14111411
} else {
1412-
// Path components present: follow spec priority order
1412+
// Path components present: prefer OAuth discovery before OpenID Connect fallbacks.
14131413
// 1. OAuth 2.0 with path insertion
14141414
push_candidate(format!("/.well-known/oauth-authorization-server/{trimmed}"));
1415-
// 2. OpenID Connect with path insertion
1415+
// 2. OAuth 2.0 with path appending
1416+
push_candidate(format!("/{trimmed}/.well-known/oauth-authorization-server"));
1417+
// 3. OpenID Connect with path insertion
14161418
push_candidate(format!("/.well-known/openid-configuration/{trimmed}"));
1417-
// 3. OpenID Connect with path appending
1419+
// 4. OpenID Connect with path appending
14181420
push_candidate(format!("/{trimmed}/.well-known/openid-configuration"));
1419-
// 4. Canonical OAuth fallback (without path suffix)
1421+
// 5. Canonical OAuth fallback (without path suffix)
14201422
push_candidate("/.well-known/oauth-authorization-server".to_string());
14211423
}
14221424

@@ -2684,61 +2686,73 @@ mod tests {
26842686

26852687
let base_url = Url::parse("https://auth.example.com/tenant1").unwrap();
26862688
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
2687-
assert_eq!(urls.len(), 4);
2689+
assert_eq!(urls.len(), 5);
26882690
assert_eq!(
26892691
urls[0].as_str(),
26902692
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1"
26912693
);
26922694
assert_eq!(
26932695
urls[1].as_str(),
2694-
"https://auth.example.com/.well-known/openid-configuration/tenant1"
2696+
"https://auth.example.com/tenant1/.well-known/oauth-authorization-server"
26952697
);
26962698
assert_eq!(
26972699
urls[2].as_str(),
2698-
"https://auth.example.com/tenant1/.well-known/openid-configuration"
2700+
"https://auth.example.com/.well-known/openid-configuration/tenant1"
26992701
);
27002702
assert_eq!(
27012703
urls[3].as_str(),
2704+
"https://auth.example.com/tenant1/.well-known/openid-configuration"
2705+
);
2706+
assert_eq!(
2707+
urls[4].as_str(),
27022708
"https://auth.example.com/.well-known/oauth-authorization-server"
27032709
);
27042710

27052711
let base_url = Url::parse("https://auth.example.com/v1/mcp/").unwrap();
27062712
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
2707-
assert_eq!(urls.len(), 4);
2713+
assert_eq!(urls.len(), 5);
27082714
assert_eq!(
27092715
urls[0].as_str(),
27102716
"https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp"
27112717
);
27122718
assert_eq!(
27132719
urls[1].as_str(),
2714-
"https://auth.example.com/.well-known/openid-configuration/v1/mcp"
2720+
"https://auth.example.com/v1/mcp/.well-known/oauth-authorization-server"
27152721
);
27162722
assert_eq!(
27172723
urls[2].as_str(),
2718-
"https://auth.example.com/v1/mcp/.well-known/openid-configuration"
2724+
"https://auth.example.com/.well-known/openid-configuration/v1/mcp"
27192725
);
27202726
assert_eq!(
27212727
urls[3].as_str(),
2728+
"https://auth.example.com/v1/mcp/.well-known/openid-configuration"
2729+
);
2730+
assert_eq!(
2731+
urls[4].as_str(),
27222732
"https://auth.example.com/.well-known/oauth-authorization-server"
27232733
);
27242734

27252735
let base_url = Url::parse("https://auth.example.com/tenant1/subtenant").unwrap();
27262736
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
2727-
assert_eq!(urls.len(), 4);
2737+
assert_eq!(urls.len(), 5);
27282738
assert_eq!(
27292739
urls[0].as_str(),
27302740
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1/subtenant"
27312741
);
27322742
assert_eq!(
27332743
urls[1].as_str(),
2734-
"https://auth.example.com/.well-known/openid-configuration/tenant1/subtenant"
2744+
"https://auth.example.com/tenant1/subtenant/.well-known/oauth-authorization-server"
27352745
);
27362746
assert_eq!(
27372747
urls[2].as_str(),
2738-
"https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration"
2748+
"https://auth.example.com/.well-known/openid-configuration/tenant1/subtenant"
27392749
);
27402750
assert_eq!(
27412751
urls[3].as_str(),
2752+
"https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration"
2753+
);
2754+
assert_eq!(
2755+
urls[4].as_str(),
27422756
"https://auth.example.com/.well-known/oauth-authorization-server"
27432757
);
27442758
}

crates/rmcp/tests/test_client_credentials.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,25 @@ async fn start_mock_server() -> (String, SocketAddr) {
136136
(base_url, addr)
137137
}
138138

139+
async fn start_path_append_metadata_server() -> (String, SocketAddr) {
140+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
141+
let addr = listener.local_addr().unwrap();
142+
let base_url = format!("http://{}", addr);
143+
144+
let app = Router::new()
145+
.route(
146+
"/mcp/.well-known/oauth-authorization-server",
147+
get(auth_server_metadata_handler),
148+
)
149+
.route("/token", post(token_handler));
150+
151+
tokio::spawn(async move {
152+
axum::serve(listener, app).await.unwrap();
153+
});
154+
155+
(base_url, addr)
156+
}
157+
139158
#[tokio::test]
140159
async fn test_client_credentials_flow_client_secret() {
141160
let (base_url, _addr) = start_mock_server().await;
@@ -162,6 +181,33 @@ async fn test_client_credentials_flow_client_secret() {
162181
assert_eq!(token, "m2m-access-token-12345");
163182
}
164183

184+
#[tokio::test]
185+
async fn test_client_credentials_discovers_path_append_oauth_metadata() {
186+
let (base_url, _addr) = start_path_append_metadata_server().await;
187+
let resource_url = format!("{base_url}/mcp");
188+
189+
let mut oauth_state = OAuthState::new(&resource_url, None).await.unwrap();
190+
191+
let config = ClientCredentialsConfig::ClientSecret {
192+
client_id: "test-m2m-client".to_string(),
193+
client_secret: "test-m2m-secret".to_string(),
194+
scopes: vec!["read".to_string()],
195+
resource: Some(resource_url),
196+
};
197+
198+
oauth_state
199+
.authenticate_client_credentials(config)
200+
.await
201+
.unwrap();
202+
203+
let manager = oauth_state
204+
.into_authorization_manager()
205+
.expect("Should be in Authorized state");
206+
207+
let token = manager.get_access_token().await.unwrap();
208+
assert_eq!(token, "m2m-access-token-12345");
209+
}
210+
165211
#[tokio::test]
166212
async fn test_client_credentials_invalid_secret() {
167213
let (base_url, _addr) = start_mock_server().await;

0 commit comments

Comments
 (0)