Skip to content

chore: add oauth2 support #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions OAUTH_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Model Context Protocol OAuth Authorization

This document describes the OAuth 2.1 authorization implementation for Model Context Protocol (MCP), following the [MCP 2025-03-26 Authorization Specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/).

## Features

- Full support for OAuth 2.1 authorization flow
- PKCE support for enhanced security
- Authorization server metadata discovery
- Dynamic client registration
- Automatic token refresh
- Authorized SSE transport implementation

## Usage Guide

### 1. Enable Features

Enable the auth feature in Cargo.toml:

```toml
[dependencies]
rmcp = { version = "0.1", features = ["auth", "transport-sse"] }
```

### 2. Create Authorization Manager

```rust
use std::sync::Arc;
use rmcp::transport::auth::AuthorizationManager;

async fn main() -> anyhow::Result<()> {
// Create authorization manager
let auth_manager = Arc::new(AuthorizationManager::new("https://api.example.com/mcp").await?);

Ok(())
}
```

### 3. Create Authorization Session and Get Authorization

```rust
use rmcp::transport::auth::AuthorizationSession;

async fn get_authorization(auth_manager: Arc<AuthorizationManager>) -> anyhow::Result<()> {
// Create authorization session
let session = AuthorizationSession::new(
auth_manager.clone(),
&["mcp"], // Requested scopes
"http://localhost:8080/callback", // Redirect URI
).await?;

// Get authorization URL and guide user to open it
let auth_url = session.get_authorization_url();
println!("Please open the following URL in your browser for authorization:\n{}", auth_url);

// Handle callback - In real applications, this is typically done in a callback server
let auth_code = "Authorization code obtained from browser after user authorization";
let credentials = session.handle_callback(auth_code).await?;

println!("Authorization successful, access token: {}", credentials.access_token);

Ok(())
}
```

### 4. Use Authorized SSE Transport

```rust
use rmcp::{ServiceExt, model::ClientInfo, transport::create_authorized_transport};

async fn connect_with_auth(auth_manager: Arc<AuthorizationManager>) -> anyhow::Result<()> {
// Create authorized SSE transport
let transport = create_authorized_transport(
"https://api.example.com/mcp",
auth_manager.clone()
).await?;

// Create client
let client_service = ClientInfo::default();
let client = client_service.serve(transport).await?;

// Use client to call APIs
let tools = client.peer().list_all_tools().await?;

for tool in tools {
println!("Tool: {} - {}", tool.name, tool.description);
}

Ok(())
}
```

### 5. Use Authorized HTTP Client

```rust
use rmcp::transport::auth::AuthorizedHttpClient;

async fn make_authorized_request(auth_manager: Arc<AuthorizationManager>) -> anyhow::Result<()> {
// Create authorized HTTP client
let client = AuthorizedHttpClient::new(auth_manager, None);

// Send authorized request
let response = client.get("https://api.example.com/resources").await?;
let resources = response.json::<Vec<Resource>>().await?;

println!("Number of resources: {}", resources.len());

Ok(())
}
```

## Complete Example

Please refer to `examples/oauth_client.rs` for a complete usage example.

## Running the Example

```bash
# Set server URL (optional)
export MCP_SERVER_URL=https://api.example.com/mcp

# Run example
cargo run --bin oauth-client
```

## Authorization Flow Description

1. **Metadata Discovery**: Client attempts to get authorization server metadata from `/.well-known/oauth-authorization-server`
2. **Client Registration**: If supported, client dynamically registers itself
3. **Authorization Request**: Build authorization URL with PKCE and guide user to access
4. **Authorization Code Exchange**: After user authorization, exchange authorization code for access token
5. **Token Usage**: Use access token for API calls
6. **Token Refresh**: Automatically use refresh token to get new access token when current one expires

## Security Considerations

- All tokens are securely stored in memory
- PKCE implementation prevents authorization code interception attacks
- Automatic token refresh support reduces user intervention
- Only accepts HTTPS connections or secure local callback URIs

## Troubleshooting

If you encounter authorization issues, check the following:

1. Ensure server supports OAuth 2.1 authorization
2. Verify callback URI matches server's allowed redirect URIs
3. Check network connection and firewall settings
4. Verify server supports metadata discovery or dynamic client registration

## References

- [MCP Authorization Specification](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/)
- [OAuth 2.1 Specification Draft](https://oauth.net/2.1/)
- [RFC 8414: OAuth 2.0 Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
- [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
6 changes: 6 additions & 0 deletions crates/rmcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ tracing = { version = "0.1" }
tokio-util = { version = "0.7" }
pin-project-lite = "0.2"
paste = { version = "1", optional = true }

# oauth2 support
oauth2 = { version = "5.0", optional = true }

# for auto generate schema
schemars = { version = "0.8", optional = true }

Expand Down Expand Up @@ -70,6 +74,8 @@ transport-sse-server = [
]
# transport-ws = ["transport-io", "dep:tokio-tungstenite"]
tower = ["dep:tower-service"]
auth = ["dep:oauth2", "dep:reqwest", "dep:url"]

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
schemars = { version = "0.8" }
Expand Down
10 changes: 10 additions & 0 deletions crates/rmcp/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ pub mod sse;
#[cfg(feature = "transport-sse")]
pub use sse::SseTransport;

#[cfg(all(feature = "transport-sse", feature = "auth"))]
pub mod sse_auth;
#[cfg(all(feature = "transport-sse", feature = "auth"))]
pub use sse_auth::{AuthorizedSseClient, create_authorized_transport};

// #[cfg(feature = "tower")]
// pub mod tower;

Expand All @@ -64,6 +69,11 @@ pub mod sse_server;
#[cfg(feature = "transport-sse-server")]
pub use sse_server::SseServer;

#[cfg(feature = "auth")]
pub mod auth;
#[cfg(feature = "auth")]
pub use auth::{AuthError, AuthorizationManager, AuthorizationSession, AuthorizedHttpClient};

// #[cfg(feature = "transport-ws")]
// pub mod ws;

Expand Down
Loading
Loading