Repository: modelcontextprotocol/rust-sdk
Pull request #157: feat: several oauth improvements: support reusing credentials. capture additional fields instead of failing
Author: login: the-rooster
Created: 2025-05-06T01:43:22Z | Merged: 2025-05-08T01:18:35Z

<!-- Provide a brief summary of your changes -->

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
I am writing an application (oneserver.ai) that uses this sdk for connecting to remote servers, and encountered a few issues with the new OAuth work:
- Currently I cannot use reuse OAuth credentials conveniently (must do the whole flow each time)
- I encounter decoding errors on dynamic client registration responses + OAuth metadata discovery due to fields that are not included in the serde struct definitions

This PR fixes both of these:
- OAuthState now has two new methods:
get_credentials: returns client_id and OAuthTokenResponse
set_credentials: sets the client_id and OAuthTokenResponse, and moves the state machine into the Authorized state for use in the transport
- Added an `additional_fields` field to both the AuthorizationMetadata struct and the ClientRegistrationResponse struct which will contain all additional fields available in the response. This lets us safely ignore fields that we don't need for our flow.
- Additionally, client_secret is now an optionally returned value in ClientRegistrationResponse (some implementations of Oauth2 dynamic client registration do not return this value it seems)

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were tested? -->
I tested this from within my application oneserver.ai against our hosted hello world remote MCP server at https://mcp.oneserver.ai and it works perfectly :)

You will notice that the current version fails when run against this server, and the new version succeeds.

## Breaking Changes
<!-- Will users need to update their code or configurations? -->
Will break some implementations that use the ClientRegistrationResponse and AuthorizationMetadata structs, but considering this oauth work is very new, I think this is okay.

## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update

## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] I have read the [MCP Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [] I have added or updated documentation as needed

## Additional context
<!-- Add any other context, implementation notes, or design decisions -->



Diff:
diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs
index cfe7b9a9d..ea1d96ae6 100644
--- a/crates/rmcp/src/transport/auth.rs
+++ b/crates/rmcp/src/transport/auth.rs
@@ -1,4 +1,5 @@
 use std::{
+    collections::HashMap,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -70,6 +71,9 @@ pub struct AuthorizationMetadata {
     pub issuer: Option<String>,
     pub jwks_uri: Option<String>,
     pub scopes_supported: Option<Vec<String>>,
+    // allow additional fields
+    #[serde(flatten)]
+    pub additional_fields: HashMap<String, serde_json::Value>,
 }
 
 /// oauth2 client config
@@ -100,6 +104,7 @@ type OAuthClient = oauth2::Client<
     oauth2::EndpointNotSet,
     oauth2::EndpointSet,
 >;
+type Credentials = (String, Option<OAuthTokenResponse>);
 
 /// oauth2 auth manager
 pub struct AuthorizationManager {
@@ -124,9 +129,12 @@ pub struct ClientRegistrationRequest {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ClientRegistrationResponse {
     pub client_id: String,
-    pub client_secret: String,
+    pub client_secret: Option<String>,
     pub client_name: String,
     pub redirect_uris: Vec<String>,
+    // allow additional fields
+    #[serde(flatten)]
+    pub additional_fields: HashMap<String, serde_json::Value>,
 }
 
 impl AuthorizationManager {
@@ -191,10 +199,22 @@ impl AuthorizationManager {
                 issuer: None,
                 jwks_uri: None,
                 scopes_supported: None,
+                additional_fields: HashMap::new(),
             })
         }
     }
 
+    /// get client id and credentials
+    pub async fn get_credentials(&self) -> Result<Credentials, AuthError> {
+        let credentials = self.credentials.read().await;
+        let client_id = self
+            .oauth_client
+            .as_ref()
+            .ok_or_else(|| AuthError::InternalError("OAuth client not configured".to_string()))?
+            .client_id();
+        Ok((client_id.to_string(), credentials.clone()))
+    }
+
     /// configure oauth2 client with client credentials
     pub fn configure_client(&mut self, config: OAuthClientConfig) -> Result<(), AuthError> {
         if self.metadata.is_none() {
@@ -287,6 +307,7 @@ impl AuthorizationManager {
                 status, error_text
             )));
         }
+
         debug!("registration response: {:?}", response);
         let reg_response = match response.json::<ClientRegistrationResponse>().await {
             Ok(response) => response,
@@ -301,7 +322,7 @@ impl AuthorizationManager {
 
         let config = OAuthClientConfig {
             client_id: reg_response.client_id,
-            client_secret: Some(reg_response.client_secret),
+            client_secret: reg_response.client_secret,
             redirect_uri: redirect_uri.to_string(),
             scopes: vec![],
         };
@@ -310,6 +331,18 @@ impl AuthorizationManager {
         Ok(config)
     }
 
+    /// use provided client id to configure oauth2 client instead of dynamic registration
+    /// this is useful when you have a stored client id from previous registration
+    pub fn configure_client_id(&mut self, client_id: &str) -> Result<(), AuthError> {
+        let config = OAuthClientConfig {
+            client_id: client_id.to_string(),
+            client_secret: None,
+            scopes: vec![],
+            redirect_uri: self.base_url.to_string(),
+        };
+        self.configure_client(config)
+    }
+
     /// generate authorization url
     pub async fn get_authorization_url(&self, scopes: &[&str]) -> Result<String, AuthError> {
         let oauth_client = self
@@ -513,6 +546,11 @@ impl AuthorizationSession {
         })
     }
 
+    /// get client_id and credentials
+    pub async fn get_credentials(&self) -> Result<Credentials, AuthError> {
+        self.auth_manager.get_credentials().await
+    }
+
     /// get authorization url
     pub fn get_authorization_url(&self) -> &str {
         &self.auth_url
@@ -590,9 +628,54 @@ impl OAuthState {
         if let Some(client) = client {
             manager.with_client(client)?;
         }
+
         Ok(OAuthState::Unauthorized(manager))
     }
 
+    /// Get client_id and OAuth credentials
+    pub async fn get_credentials(&self) -> Result<Credentials, AuthError> {
+        // return client_id and credentials
+        match self {
+            OAuthState::Unauthorized(manager) | OAuthState::Authorized(manager) => {
+                manager.get_credentials().await
+            }
+            OAuthState::Session(session) => session.get_credentials().await,
+            OAuthState::AuthorizedHttpClient(client) => client.auth_manager.get_credentials().await,
+        }
+    }
+
+    /// Manually set credentials and move into authorized state
+    /// Useful if you're caching credentials externally and wish to reuse them
+    pub async fn set_credentials(
+        &mut self,
+        client_id: &str,
+        credentials: OAuthTokenResponse,
+    ) -> Result<(), AuthError> {
+        if let OAuthState::Unauthorized(manager) = self {
+            let mut manager = std::mem::replace(
+                manager,
+                AuthorizationManager::new("http://localhost").await?,
+            );
+
+            // write credentials
+            *manager.credentials.write().await = Some(credentials);
+
+            // discover metadata
+            let metadata = manager.discover_metadata().await?;
+            manager.metadata = Some(metadata);
+
+            // set client id and secret
+            manager.configure_client_id(client_id)?;
+
+            *self = OAuthState::Authorized(manager);
+            Ok(())
+        } else {
+            Err(AuthError::InternalError(
+                "Cannot set credentials in this state".to_string(),
+            ))
+        }
+    }
+
     /// start authorization
     pub async fn start_authorization(
         &mut self,
diff --git a/examples/servers/src/mcp_oauth_server.rs b/examples/servers/src/mcp_oauth_server.rs
index 2a6472d33..8047434f8 100644
--- a/examples/servers/src/mcp_oauth_server.rs
+++ b/examples/servers/src/mcp_oauth_server.rs
@@ -525,6 +525,7 @@ async fn oauth_authorization_server() -> impl IntoResponse {
         registration_endpoint: format!("http://{}/oauth/register", BIND_ADDRESS),
         issuer: Some(BIND_ADDRESS.to_string()),
         jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
+        additional_fields: HashMap::new(),
     };
     debug!("metadata: {:?}", metadata);
     (StatusCode::OK, Json(metadata))
@@ -567,9 +568,10 @@ async fn oauth_register(
     // return client information
     let response = ClientRegistrationResponse {
         client_id,
-        client_secret,
+        client_secret: Some(client_secret),
         client_name: req.client_name,
         redirect_uris: req.redirect_uris,
+        additional_fields: HashMap::new(),
     };
 
     (StatusCode::CREATED, Json(response)).into_response()


Reviews:
user: login: jokemanfire
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T02:41:33Z
user: login: the-rooster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T02:45:18Z
user: login: the-rooster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T02:47:22Z
user: login: jokemanfire
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T07:26:04Z

Review comments:
diff_hunk: @@ -590,9 +632,52 @@ impl OAuthState {
         if let Some(client) = client {
             manager.with_client(client)?;
         }
+
         Ok(OAuthState::Unauthorized(manager))
     }
 
+    /// Get client_id and OAuth credentials
+    pub async fn get_credentials(&self) -> Result<(String, Option<OAuthTokenResponse>), AuthError> {
path: crates/rmcp/src/transport/auth.rs
user: login: jokemanfire
body:     match self {
        OAuthState::Unauthorized(manager)
        | OAuthState::Session(manager)
        | OAuthState::Authorized(manager) => manager.get_credentials().await,
        OAuthState::AuthorizedHttpClient(client) => 
            client.auth_manager.get_credentials().await,
    }
created_at: 2025-05-06T02:32:07Z
author_association: MEMBER
diff_hunk: @@ -513,6 +549,12 @@ impl AuthorizationSession {
         })
     }
 
+
+    /// get client_id and credentials
+    pub async fn get_credentials(&self) -> Result<(String, Option<OAuthTokenResponse>), AuthError> {
path: crates/rmcp/src/transport/auth.rs
user: login: jokemanfire
body: use alias
`type Credentials = (String, Option<OAuthTokenResponse>);`
created_at: 2025-05-06T02:41:17Z
author_association: MEMBER
diff_hunk: @@ -590,9 +632,52 @@ impl OAuthState {
         if let Some(client) = client {
             manager.with_client(client)?;
         }
+
         Ok(OAuthState::Unauthorized(manager))
     }
 
+    /// Get client_id and OAuth credentials
+    pub async fn get_credentials(&self) -> Result<(String, Option<OAuthTokenResponse>), AuthError> {
path: crates/rmcp/src/transport/auth.rs
user: login: the-rooster
body: Session uses AuthorizationSession, but I can combine the two that use manager!
created_at: 2025-05-06T02:45:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -590,9 +632,52 @@ impl OAuthState {
         if let Some(client) = client {
             manager.with_client(client)?;
         }
+
         Ok(OAuthState::Unauthorized(manager))
     }
 
+    /// Get client_id and OAuth credentials
+    pub async fn get_credentials(&self) -> Result<(String, Option<OAuthTokenResponse>), AuthError> {
path: crates/rmcp/src/transport/auth.rs
user: login: the-rooster
body: done
created_at: 2025-05-06T02:47:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: jokemanfire
created_at: 2025-05-06T06:24:58Z
body: pls fix the lint.
author_association: MEMBER
user: login: jokemanfire
created_at: 2025-05-06T07:45:08Z
body: test_with_js_client test has some problems , it should be fixed firstly @4t145 
author_association: MEMBER
user: login: 4t145
created_at: 2025-05-06T11:02:38Z
body: @jokemanfire I'll have a look
author_association: CONTRIBUTOR
user: login: the-rooster
created_at: 2025-05-06T14:13:44Z
body: @jokemanfire fixed!
author_association: CONTRIBUTOR
user: login: jokemanfire
created_at: 2025-05-07T00:58:49Z
body: LGTM , but just wait for the CI fixed.
author_association: MEMBER
user: login: 4t145
created_at: 2025-05-07T18:35:16Z
body: I guess you may merge the lasted code in main branch so we can test it in a test fixed version?
author_association: CONTRIBUTOR
user: login: the-rooster
created_at: 2025-05-07T19:38:14Z
body: merged
author_association: CONTRIBUTOR