Skip to content

Commit 3283594

Browse files
committed
feat!: return source from metadata resolution
1 parent c493017 commit 3283594

4 files changed

Lines changed: 138 additions & 92 deletions

File tree

conformance/src/bin/client.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,8 @@ async fn migration_token(
509509
return Ok(manager.get_access_token().await?);
510510
}
511511

512-
let metadata = manager.discover_metadata().await?;
513-
manager.set_metadata(metadata);
512+
let resolution = manager.resolve_metadata().await?;
513+
manager.set_metadata(resolution.metadata);
514514
manager
515515
.register_client("conformance-client", REDIRECT_URI, &[])
516516
.await?;
@@ -609,9 +609,9 @@ async fn run_client_credentials_basic(
609609
.unwrap_or("conformance-test-secret");
610610

611611
let mut manager = AuthorizationManager::new(server_url).await?;
612-
let metadata = manager.discover_metadata().await?;
613-
let token_endpoint = metadata.token_endpoint.clone();
614-
manager.set_metadata(metadata);
612+
let resolution = manager.resolve_metadata().await?;
613+
let token_endpoint = resolution.metadata.token_endpoint.clone();
614+
manager.set_metadata(resolution.metadata);
615615

616616
let http = reqwest::Client::new();
617617
let resp = http

crates/rmcp/src/transport/auth.rs

Lines changed: 70 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ pub enum AuthorizationMetadataSource {
591591
/// URL (`/authorize`, `/token`, `/register`) for compatibility with the
592592
/// 2025-03-26 MCP spec's default-endpoint fallback. The server gave no
593593
/// evidence that it supports OAuth.
594+
///
595+
/// [Newer MCP revisions] require metadata discovery and do not define an
596+
/// endpoint-synthesis fallback.
597+
///
598+
/// [Newer MCP revisions]: https://modelcontextprotocol.io/specification/draft/basic/authorization/authorization-server-discovery#protected-resource-metadata-discovery-requirements
594599
LegacyEndpointFallback,
595600
}
596601

@@ -602,10 +607,10 @@ impl AuthorizationMetadataSource {
602607
}
603608
}
604609

605-
/// [`AuthorizationMetadata`] together with its discovery provenance.
610+
/// [`AuthorizationMetadata`] together with how it was resolved.
606611
#[derive(Debug, Clone)]
607612
#[non_exhaustive]
608-
pub struct DiscoveredAuthorizationMetadata {
613+
pub struct AuthorizationMetadataResolution {
609614
pub metadata: AuthorizationMetadata,
610615
pub source: AuthorizationMetadataSource,
611616
}
@@ -1268,8 +1273,10 @@ impl AuthorizationManager {
12681273

12691274
/// Set OAuth2 authorization metadata
12701275
///
1271-
/// This should be called after discovering metadata via `discover_metadata()`
1272-
/// and before creating an `AuthorizationSession`.
1276+
/// This should be called with
1277+
/// [`AuthorizationMetadataResolution::metadata`] after
1278+
/// [`Self::resolve_metadata`] and before creating an
1279+
/// [`AuthorizationSession`].
12731280
pub fn set_metadata(&mut self, metadata: AuthorizationMetadata) {
12741281
self.metadata = Some(metadata);
12751282
}
@@ -1284,8 +1291,8 @@ impl AuthorizationManager {
12841291
&& stored.token_response.is_some()
12851292
{
12861293
if self.metadata.is_none() {
1287-
let metadata = self.discover_metadata().await?;
1288-
self.metadata = Some(metadata);
1294+
let resolution = self.resolve_metadata().await?;
1295+
self.metadata = Some(resolution.metadata);
12891296
}
12901297

12911298
if let (Some(stored_issuer), Some(current_issuer)) =
@@ -1352,43 +1359,48 @@ impl AuthorizationManager {
13521359
Ok(())
13531360
}
13541361

1355-
/// discover oauth2 metadata (per SEP-985: Protected Resource Metadata first, then direct OAuth)
1362+
/// Resolve OAuth 2.0 metadata and report how it was obtained.
13561363
///
1357-
/// When no metadata can be discovered, this falls back to legacy default
1358-
/// endpoints derived from the base URL rather than returning an error, so
1359-
/// a successful result does not prove the server supports OAuth. Callers
1360-
/// that need to tell verified discovery apart from the synthesized
1361-
/// fallback should use [`Self::discover_metadata_with_source`] instead.
1362-
pub async fn discover_metadata(&self) -> Result<AuthorizationMetadata, AuthError> {
1363-
Ok(self.discover_metadata_with_source().await?.metadata)
1364-
}
1365-
1366-
/// Discover oauth2 metadata along with how it was obtained.
1364+
/// Discovery follows SEP-985: protected resource metadata first, then
1365+
/// direct OAuth 2.0 Authorization Server Metadata or OpenID Connect
1366+
/// Discovery. When discovery finds nothing, the result contains legacy
1367+
/// default endpoints derived from the base URL and
1368+
/// [`AuthorizationMetadataSource::LegacyEndpointFallback`].
13671369
///
1368-
/// Unlike [`Self::discover_metadata`], the returned
1369-
/// [`AuthorizationMetadataSource`] lets callers distinguish metadata the
1370-
/// server actually published from the legacy default-endpoint fallback
1371-
/// that is synthesized when discovery finds nothing
1372-
/// ([`AuthorizationMetadataSource::LegacyEndpointFallback`]).
1373-
pub async fn discover_metadata_with_source(
1374-
&self,
1375-
) -> Result<DiscoveredAuthorizationMetadata, AuthError> {
1370+
/// # Examples
1371+
///
1372+
/// ```no_run
1373+
/// use rmcp::transport::auth::{AuthorizationManager, AuthorizationMetadataSource};
1374+
///
1375+
/// # async fn resolve() -> Result<(), Box<dyn std::error::Error>> {
1376+
/// let mut manager = AuthorizationManager::new("https://mcp.example.com").await?;
1377+
/// let resolution = manager.resolve_metadata().await?;
1378+
///
1379+
/// if resolution.source == AuthorizationMetadataSource::LegacyEndpointFallback {
1380+
/// println!("the server did not publish OAuth metadata");
1381+
/// }
1382+
///
1383+
/// manager.set_metadata(resolution.metadata);
1384+
/// # Ok(())
1385+
/// # }
1386+
/// ```
1387+
pub async fn resolve_metadata(&self) -> Result<AuthorizationMetadataResolution, AuthError> {
13761388
if let Some(metadata) = self.discover_oauth_server_via_resource_metadata().await? {
1377-
return Ok(DiscoveredAuthorizationMetadata {
1389+
return Ok(AuthorizationMetadataResolution {
13781390
metadata,
13791391
source: AuthorizationMetadataSource::ProtectedResourceMetadata,
13801392
});
13811393
}
13821394

13831395
if let Some(metadata) = self.try_discover_oauth_server(&self.base_url).await? {
1384-
return Ok(DiscoveredAuthorizationMetadata {
1396+
return Ok(AuthorizationMetadataResolution {
13851397
metadata,
13861398
source: AuthorizationMetadataSource::AuthorizationServerMetadata,
13871399
});
13881400
}
13891401

13901402
debug!("falling back to legacy OAuth endpoints derived from the base URL");
1391-
Ok(DiscoveredAuthorizationMetadata {
1403+
Ok(AuthorizationMetadataResolution {
13921404
metadata: Self::legacy_authorization_metadata(&self.base_url),
13931405
source: AuthorizationMetadataSource::LegacyEndpointFallback,
13941406
})
@@ -3473,8 +3485,8 @@ impl OAuthState {
34733485

34743486
*manager.current_scopes.write().await = granted_scopes.clone();
34753487

3476-
let metadata = manager.discover_metadata().await?;
3477-
manager.metadata = Some(metadata);
3488+
let resolution = manager.resolve_metadata().await?;
3489+
manager.metadata = Some(resolution.metadata);
34783490

34793491
let stored = StoredCredentials {
34803492
client_id: client_id.to_string(),
@@ -3528,8 +3540,8 @@ impl OAuthState {
35283540
));
35293541
};
35303542
debug!("start discovery");
3531-
let metadata = match manager.discover_metadata().await {
3532-
Ok(metadata) => metadata,
3543+
let metadata = match manager.resolve_metadata().await {
3544+
Ok(resolution) => resolution.metadata,
35333545
Err(e) => {
35343546
*self = OAuthState::Unauthorized(manager);
35353547
return Err(e);
@@ -3721,8 +3733,8 @@ impl OAuthState {
37213733
};
37223734

37233735
// Discover metadata
3724-
let metadata = manager.discover_metadata().await?;
3725-
manager.metadata = Some(metadata);
3736+
let resolution = manager.resolve_metadata().await?;
3737+
manager.metadata = Some(resolution.metadata);
37263738

37273739
// Validate server supports the requested auth method
37283740
manager.validate_client_credentials_metadata(&config)?;
@@ -3857,7 +3869,7 @@ mod tests {
38573869
.await
38583870
.unwrap();
38593871

3860-
let metadata = manager.discover_metadata().await.unwrap();
3872+
let metadata = manager.resolve_metadata().await.unwrap().metadata;
38613873

38623874
assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
38633875
assert_eq!(
@@ -3920,7 +3932,7 @@ mod tests {
39203932
.await
39213933
.unwrap();
39223934

3923-
let metadata = manager.discover_metadata().await.unwrap();
3935+
let metadata = manager.resolve_metadata().await.unwrap().metadata;
39243936

39253937
assert_eq!(
39263938
(
@@ -3979,7 +3991,7 @@ mod tests {
39793991
.await
39803992
.unwrap();
39813993

3982-
let error = manager.discover_metadata().await.unwrap_err();
3994+
let error = manager.resolve_metadata().await.unwrap_err();
39833995

39843996
assert!(
39853997
matches!(
@@ -4102,7 +4114,7 @@ mod tests {
41024114
.await
41034115
.unwrap();
41044116

4105-
let metadata = manager.discover_metadata().await.unwrap();
4117+
let metadata = manager.resolve_metadata().await.unwrap().metadata;
41064118

41074119
assert_eq!(
41084120
(
@@ -4137,7 +4149,7 @@ mod tests {
41374149
}
41384150

41394151
#[tokio::test]
4140-
async fn discover_metadata_falls_back_to_legacy_default_endpoints() {
4152+
async fn resolve_metadata_reports_legacy_fallback_when_nothing_is_discovered() {
41414153
let client = RecordingOAuthHttpClient::with_responses(vec![
41424154
empty_response(404),
41434155
empty_response(404),
@@ -4152,20 +4164,22 @@ mod tests {
41524164
.await
41534165
.unwrap();
41544166

4155-
let metadata = manager.discover_metadata().await.unwrap();
4167+
let resolution = manager.resolve_metadata().await.unwrap();
41564168

41574169
assert_eq!(
41584170
(
4159-
metadata.authorization_endpoint.as_str(),
4160-
metadata.token_endpoint.as_str(),
4161-
metadata.registration_endpoint.as_deref(),
4171+
resolution.source,
4172+
resolution.metadata.authorization_endpoint.as_str(),
4173+
resolution.metadata.token_endpoint.as_str(),
4174+
resolution.metadata.registration_endpoint.as_deref(),
41624175
client
41634176
.requests()
41644177
.iter()
41654178
.map(|request| request.uri.as_str())
41664179
.collect::<Vec<_>>(),
41674180
),
41684181
(
4182+
AuthorizationMetadataSource::LegacyEndpointFallback,
41694183
"https://legacy.example.com/authorize",
41704184
"https://legacy.example.com/token",
41714185
Some("https://legacy.example.com/register"),
@@ -4181,37 +4195,7 @@ mod tests {
41814195
}
41824196

41834197
#[tokio::test]
4184-
async fn discover_metadata_with_source_reports_legacy_fallback_when_nothing_is_discovered() {
4185-
let client = RecordingOAuthHttpClient::with_responses(vec![
4186-
empty_response(404),
4187-
empty_response(404),
4188-
empty_response(404),
4189-
empty_response(404),
4190-
empty_response(404),
4191-
]);
4192-
let manager = AuthorizationManager::new_with_oauth_http_client(
4193-
"https://legacy.example.com/",
4194-
Arc::new(client),
4195-
)
4196-
.await
4197-
.unwrap();
4198-
4199-
let discovered = manager.discover_metadata_with_source().await.unwrap();
4200-
4201-
assert_eq!(
4202-
(
4203-
discovered.source,
4204-
discovered.metadata.authorization_endpoint.as_str(),
4205-
),
4206-
(
4207-
AuthorizationMetadataSource::LegacyEndpointFallback,
4208-
"https://legacy.example.com/authorize",
4209-
)
4210-
);
4211-
}
4212-
4213-
#[tokio::test]
4214-
async fn discover_metadata_with_source_reports_protected_resource_metadata() {
4198+
async fn resolve_metadata_reports_protected_resource_metadata() {
42154199
let challenge = oauth2::http::Response::builder()
42164200
.status(401)
42174201
.header(
@@ -4245,12 +4229,12 @@ mod tests {
42454229
.await
42464230
.unwrap();
42474231

4248-
let discovered = manager.discover_metadata_with_source().await.unwrap();
4232+
let resolution = manager.resolve_metadata().await.unwrap();
42494233

42504234
assert_eq!(
42514235
(
4252-
discovered.source,
4253-
discovered.metadata.token_endpoint.as_str(),
4236+
resolution.source,
4237+
resolution.metadata.token_endpoint.as_str(),
42544238
),
42554239
(
42564240
AuthorizationMetadataSource::ProtectedResourceMetadata,
@@ -4260,7 +4244,7 @@ mod tests {
42604244
}
42614245

42624246
#[tokio::test]
4263-
async fn discover_metadata_with_source_reports_authorization_server_metadata() {
4247+
async fn resolve_metadata_reports_authorization_server_metadata() {
42644248
let client = RecordingOAuthHttpClient::with_responses(vec![
42654249
empty_response(404),
42664250
empty_response(404),
@@ -4281,12 +4265,12 @@ mod tests {
42814265
.await
42824266
.unwrap();
42834267

4284-
let discovered = manager.discover_metadata_with_source().await.unwrap();
4268+
let resolution = manager.resolve_metadata().await.unwrap();
42854269

42864270
assert_eq!(
42874271
(
4288-
discovered.source,
4289-
discovered.metadata.token_endpoint.as_str(),
4272+
resolution.source,
4273+
resolution.metadata.token_endpoint.as_str(),
42904274
),
42914275
(
42924276
AuthorizationMetadataSource::AuthorizationServerMetadata,
@@ -4398,7 +4382,7 @@ mod tests {
43984382
)
43994383
.await
44004384
.unwrap();
4401-
manager.metadata = Some(manager.discover_metadata().await.unwrap());
4385+
manager.metadata = Some(manager.resolve_metadata().await.unwrap().metadata);
44024386

44034387
let request = AuthorizationRequest::new("http://localhost:8080/callback")
44044388
.with_preregistered_client("preregistered-client");
@@ -4853,7 +4837,7 @@ mod tests {
48534837
.await
48544838
.unwrap();
48554839

4856-
let metadata = manager.discover_metadata().await.unwrap();
4840+
let metadata = manager.resolve_metadata().await.unwrap().metadata;
48574841
let requests = client.requests();
48584842

48594843
assert_eq!(
@@ -4910,7 +4894,7 @@ mod tests {
49104894
.await
49114895
.unwrap();
49124896

4913-
let metadata = manager.discover_metadata().await.unwrap();
4897+
let metadata = manager.resolve_metadata().await.unwrap().metadata;
49144898

49154899
assert_eq!(
49164900
(
@@ -4959,7 +4943,7 @@ mod tests {
49594943
.await
49604944
.unwrap();
49614945

4962-
let error = manager.discover_metadata().await.unwrap_err();
4946+
let error = manager.resolve_metadata().await.unwrap_err();
49634947

49644948
assert!(
49654949
matches!(error, AuthError::MetadataError(ref message) if message.contains("resource mismatch")),
@@ -4994,7 +4978,7 @@ mod tests {
49944978
.await
49954979
.unwrap();
49964980

4997-
let error = manager.discover_metadata().await.unwrap_err();
4981+
let error = manager.resolve_metadata().await.unwrap_err();
49984982

49994983
assert!(
50004984
matches!(error, AuthError::MetadataError(ref message) if message.contains("missing required resource")),

0 commit comments

Comments
 (0)