11-25-2025 compliant Auth (#651)
* fix: correct discovery for AS metadata * fix: add commitlint to dev container * feat: add RFC 8707 support for resource parameter * feat: pkce method verification * feat(auth): implement SEP-835 scope handling and 403 upgrade flow - add WWWAuthenticateParams for parsing scope and resource_metadata from headers - add ScopeUpgradeConfig and scope tracking in AuthorizationManager - add InsufficientScopeError and 403 handling in streamable HTTP client - add scope union computation for progressive authorization - export new public types: AuthClient, ScopeUpgradeConfig, WWWAuthenticateParams Co-authored-by: fizy069 <fizy069@users.noreply.github.com> * fix: reorg auth tests * feat: add error to www-authenticate header parsing * feat: consider protected resource metadata in scope selection * fix: reorganize auth tests * feat: add examples and docs for updated auth --------- Co-authored-by: fizy069 <fizy069@users.noreply.github.com>
This commit is contained in:
parent
a1c66a8a36
commit
61845d61c4
8 changed files with 1037 additions and 297 deletions
|
|
@ -1,41 +1,41 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
||||||
{
|
{
|
||||||
"name": "Rust",
|
"name": "Rust",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
|
"image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {},
|
"ghcr.io/devcontainers/features/node:1": {},
|
||||||
"ghcr.io/devcontainers/features/python:1": {
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
"version": "3.10",
|
"version": "3.10",
|
||||||
"toolsToInstall": "uv"
|
"toolsToInstall": "uv"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Configure tool-specific properties.
|
// Configure tool-specific properties.
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"[rust]": {
|
"[rust]": {
|
||||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
"postCreateCommand": "uv venv"
|
"postCreateCommand": "uv venv && npm install -g @commitlint/config-conventional"
|
||||||
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
||||||
// "mounts": [
|
// "mounts": [
|
||||||
// {
|
// {
|
||||||
// "source": "devcontainer-cargo-cache-${devcontainerId}",
|
// "source": "devcontainer-cargo-cache-${devcontainerId}",
|
||||||
// "target": "/usr/local/cargo",
|
// "target": "/usr/local/cargo",
|
||||||
// "type": "volume"
|
// "type": "volume"
|
||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
// "features": {},
|
// "features": {},
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
// "forwardPorts": [],
|
// "forwardPorts": [],
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
// "remoteUser": "root"
|
// "remoteUser": "root"
|
||||||
}
|
}
|
||||||
|
|
@ -107,9 +107,9 @@ pub mod auth;
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "auth")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "auth")))]
|
||||||
pub use auth::{
|
pub use auth::{
|
||||||
AuthError, AuthorizationManager, AuthorizationSession, AuthorizedHttpClient, CredentialStore,
|
AuthClient, AuthError, AuthorizationManager, AuthorizationSession, AuthorizedHttpClient,
|
||||||
InMemoryCredentialStore, InMemoryStateStore, StateStore, StoredAuthorizationState,
|
CredentialStore, InMemoryCredentialStore, InMemoryStateStore, ScopeUpgradeConfig, StateStore,
|
||||||
StoredCredentials,
|
StoredAuthorizationState, StoredCredentials, WWWAuthenticateParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
// #[cfg(feature = "transport-ws")]
|
// #[cfg(feature = "transport-ws")]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -120,6 +120,22 @@ impl StreamableHttpClient for reqwest::Client {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if response.status() == reqwest::StatusCode::FORBIDDEN {
|
||||||
|
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
||||||
|
let header_str = header.to_str().map_err(|_| {
|
||||||
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
|
"invalid www-authenticate header value",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let scope = extract_scope_from_header(header_str);
|
||||||
|
return Err(StreamableHttpError::InsufficientScope(
|
||||||
|
InsufficientScopeError {
|
||||||
|
www_authenticate_header: header_str.to_string(),
|
||||||
|
required_scope: scope,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if matches!(
|
if matches!(
|
||||||
status,
|
status,
|
||||||
|
|
@ -197,3 +213,81 @@ impl StreamableHttpClientTransport<reqwest::Client> {
|
||||||
StreamableHttpClientTransport::with_client(reqwest::Client::default(), config)
|
StreamableHttpClientTransport::with_client(reqwest::Client::default(), config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// extract scope parameter from WWW-Authenticate header
|
||||||
|
fn extract_scope_from_header(header: &str) -> Option<String> {
|
||||||
|
let header_lowercase = header.to_ascii_lowercase();
|
||||||
|
let scope_key = "scope=";
|
||||||
|
|
||||||
|
if let Some(pos) = header_lowercase.find(scope_key) {
|
||||||
|
let start = pos + scope_key.len();
|
||||||
|
let value_slice = &header[start..];
|
||||||
|
|
||||||
|
if let Some(stripped) = value_slice.strip_prefix('"') {
|
||||||
|
if let Some(end_quote) = stripped.find('"') {
|
||||||
|
return Some(stripped[..end_quote].to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let end = value_slice
|
||||||
|
.find(|c: char| c == ',' || c == ';' || c.is_whitespace())
|
||||||
|
.unwrap_or(value_slice.len());
|
||||||
|
if end > 0 {
|
||||||
|
return Some(value_slice[..end].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::extract_scope_from_header;
|
||||||
|
use crate::transport::streamable_http_client::InsufficientScopeError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_scope_quoted() {
|
||||||
|
let header = r#"Bearer error="insufficient_scope", scope="files:read files:write""#;
|
||||||
|
assert_eq!(
|
||||||
|
extract_scope_from_header(header),
|
||||||
|
Some("files:read files:write".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_scope_unquoted() {
|
||||||
|
let header = r#"Bearer scope=read:data, error="insufficient_scope""#;
|
||||||
|
assert_eq!(
|
||||||
|
extract_scope_from_header(header),
|
||||||
|
Some("read:data".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_scope_missing() {
|
||||||
|
let header = r#"Bearer error="invalid_token""#;
|
||||||
|
assert_eq!(extract_scope_from_header(header), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_scope_empty_header() {
|
||||||
|
assert_eq!(extract_scope_from_header("Bearer"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insufficient_scope_error_can_upgrade() {
|
||||||
|
let with_scope = InsufficientScopeError {
|
||||||
|
www_authenticate_header: "Bearer scope=\"admin\"".to_string(),
|
||||||
|
required_scope: Some("admin".to_string()),
|
||||||
|
};
|
||||||
|
assert!(with_scope.can_upgrade());
|
||||||
|
assert_eq!(with_scope.get_required_scope(), Some("admin"));
|
||||||
|
|
||||||
|
let without_scope = InsufficientScopeError {
|
||||||
|
www_authenticate_header: "Bearer error=\"insufficient_scope\"".to_string(),
|
||||||
|
required_scope: None,
|
||||||
|
};
|
||||||
|
assert!(!without_scope.can_upgrade());
|
||||||
|
assert_eq!(without_scope.get_required_scope(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,24 @@ pub struct AuthRequiredError {
|
||||||
pub www_authenticate_header: String,
|
pub www_authenticate_header: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InsufficientScopeError {
|
||||||
|
pub www_authenticate_header: String,
|
||||||
|
pub required_scope: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InsufficientScopeError {
|
||||||
|
/// check if scope upgrade is possible (i.e., we know what scope is required)
|
||||||
|
pub fn can_upgrade(&self) -> bool {
|
||||||
|
self.required_scope.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the required scope for upgrade
|
||||||
|
pub fn get_required_scope(&self) -> Option<&str> {
|
||||||
|
self.required_scope.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum StreamableHttpError<E: std::error::Error + Send + Sync + 'static> {
|
pub enum StreamableHttpError<E: std::error::Error + Send + Sync + 'static> {
|
||||||
#[error("SSE error: {0}")]
|
#[error("SSE error: {0}")]
|
||||||
|
|
@ -56,6 +74,8 @@ pub enum StreamableHttpError<E: std::error::Error + Send + Sync + 'static> {
|
||||||
Auth(#[from] crate::transport::auth::AuthError),
|
Auth(#[from] crate::transport::auth::AuthError),
|
||||||
#[error("Auth required")]
|
#[error("Auth required")]
|
||||||
AuthRequired(AuthRequiredError),
|
AuthRequired(AuthRequiredError),
|
||||||
|
#[error("Insufficient scope")]
|
||||||
|
InsufficientScope(InsufficientScopeError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
#[derive(Debug, Clone, Error)]
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
# Model Context Protocol OAuth Authorization
|
# 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://modelcontextprotocol.io/specification/2025-03-26/basic/authorization/).
|
This document describes the OAuth 2.1 authorization implementation for Model Context Protocol (MCP), following the [MCP 2025-11-25 Authorization Specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization/).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Full support for OAuth 2.1 authorization flow
|
- Full support for OAuth 2.1 authorization flow with PKCE (S256)
|
||||||
- PKCE support for enhanced security
|
- RFC 8707 resource parameter binding
|
||||||
- Authorization server metadata discovery
|
- Protected Resource Metadata discovery (RFC 9728)
|
||||||
- Dynamic client registration
|
- Authorization Server Metadata discovery (RFC 8414 + OpenID Connect)
|
||||||
|
- Dynamic client registration (RFC 7591)
|
||||||
|
- Client ID Metadata Documents (CIMD) (SEP-991 / Client ID Metadata Documents )
|
||||||
|
- Scope selection from WWW-Authenticate, Protected Resource Metadata, and AS metadata
|
||||||
|
- Scope upgrade on 403 insufficient_scope (SEP-835)
|
||||||
- Automatic token refresh
|
- Automatic token refresh
|
||||||
- Authorized HTTP Client implementation
|
- Authorized HTTP Client implementation
|
||||||
|
|
||||||
|
|
@ -24,32 +28,43 @@ rmcp = { version = "0.1", features = ["auth", "transport-streamable-http-client-
|
||||||
|
|
||||||
### 2. Use OAuthState
|
### 2. Use OAuthState
|
||||||
|
|
||||||
|
The `OAuthState` state machine manages the full authorization lifecycle. When no
|
||||||
|
scopes are provided, the SDK automatically selects scopes from the server's
|
||||||
|
WWW-Authenticate header, Protected Resource Metadata, or AS metadata.
|
||||||
|
|
||||||
```rust ignore
|
```rust ignore
|
||||||
// Initialize oauth state machine
|
// initialize oauth state machine
|
||||||
let mut oauth_state = OAuthState::new(&server_url, None)
|
let mut oauth_state = OAuthState::new(&server_url, None)
|
||||||
.await
|
.await
|
||||||
.context("Failed to initialize oauth state machine")?;
|
.context("Failed to initialize oauth state machine")?;
|
||||||
|
|
||||||
|
// start authorization - pass empty scopes to let the SDK auto-select
|
||||||
oauth_state
|
oauth_state
|
||||||
.start_authorization(&["mcp", "profile", "email"], MCP_REDIRECT_URI)
|
.start_authorization(&[], MCP_REDIRECT_URI, Some("My MCP Client"))
|
||||||
.await
|
.await
|
||||||
.context("Failed to start authorization")?;
|
.context("Failed to start authorization")?;
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Get authorization url and do callback
|
If you know the scopes you need, you can still pass them explicitly:
|
||||||
|
|
||||||
```rust ignore
|
```rust ignore
|
||||||
// Get authorization URL and guide user to open it
|
oauth_state
|
||||||
|
.start_authorization(&["mcp", "profile"], MCP_REDIRECT_URI, Some("My MCP Client"))
|
||||||
|
.await
|
||||||
|
.context("Failed to start authorization")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Get authorization url and handle callback
|
||||||
|
|
||||||
|
```rust ignore
|
||||||
|
// get authorization URL and guide user to open it
|
||||||
let auth_url = oauth_state.get_authorization_url().await?;
|
let auth_url = oauth_state.get_authorization_url().await?;
|
||||||
println!("Please open the following URL in your browser for authorization:\n{}", auth_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
|
// handle callback - in real applications, this is typically done in a callback server
|
||||||
let auth_code = "Authorization code (`code` param) obtained from browser after user authorization";
|
let auth_code = "Authorization code (`code` param) obtained from browser after user authorization";
|
||||||
let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
|
let csrf_token = "CSRF token (`state` param) obtained from browser after user authorization";
|
||||||
let credentials = oauth_state.handle_callback(auth_code, csrf_token).await?;
|
oauth_state.handle_callback(auth_code, csrf_token).await?;
|
||||||
|
|
||||||
println!("Authorization successful, access token: {}", credentials.access_token);
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Use Authorized Streamable HTTP Transport and create client
|
### 4. Use Authorized Streamable HTTP Transport and create client
|
||||||
|
|
@ -64,15 +79,27 @@ rmcp = { version = "0.1", features = ["auth", "transport-streamable-http-client-
|
||||||
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
|
StreamableHttpClientTransportConfig::with_uri(MCP_SERVER_URL),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create client and connect to MCP server
|
// create client and connect to MCP server
|
||||||
let client_service = ClientInfo::default();
|
let client_service = ClientInfo::default();
|
||||||
let client = client_service.serve(transport).await?;
|
let client = client_service.serve(transport).await?;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Use Authorized HTTP Client after authorized
|
### 5. Handle scope upgrades
|
||||||
|
|
||||||
|
If a server returns 403 with `insufficient_scope`, you can request a scope
|
||||||
|
upgrade. The SDK computes the union of current and required scopes and
|
||||||
|
transitions back to the session state for re-authorization.
|
||||||
|
|
||||||
```rust ignore
|
```rust ignore
|
||||||
let client = oauth_state.to_authorized_http_client().await?;
|
match oauth_state.request_scope_upgrade("admin:write", MCP_REDIRECT_URI).await {
|
||||||
|
Ok(auth_url) => {
|
||||||
|
// open auth_url in browser, handle callback as before
|
||||||
|
println!("Re-authorize at: {}", auth_url);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Scope upgrade failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Complete Examples
|
## Complete Examples
|
||||||
|
|
@ -92,19 +119,24 @@ cargo run -p mcp-client-examples --example clients_oauth_client
|
||||||
|
|
||||||
## Authorization Flow Description
|
## Authorization Flow Description
|
||||||
|
|
||||||
1. **Metadata Discovery**: Client attempts to get authorization server metadata from `/.well-known/oauth-authorization-server`
|
1. **Resource Metadata Discovery**: Client probes the server and extracts `WWW-Authenticate` parameters including `resource_metadata` URL and `scope`
|
||||||
2. **Client Registration**: If supported, client dynamically registers itself
|
2. **Protected Resource Metadata**: Client fetches resource server metadata (RFC 9728) to find authorization server(s) and supported scopes
|
||||||
3. **Authorization Request**: Build authorization URL with PKCE and guide user to access
|
3. **AS Metadata Discovery**: Client discovers authorization server metadata via RFC 8414 and OpenID Connect well-known endpoints
|
||||||
4. **Authorization Code Exchange**: After user authorization, exchange authorization code for access token
|
4. **Client Registration**: If supported, client dynamically registers itself (or uses URL-based Client ID via SEP-991)
|
||||||
5. **Token Usage**: Use access token for API calls
|
5. **Scope Selection**: SDK picks scopes from WWW-Authenticate > PRM > AS metadata > caller defaults
|
||||||
6. **Token Refresh**: Automatically use refresh token to get new access token when current one expires
|
6. **Authorization Request**: Build authorization URL with PKCE (S256) and RFC 8707 resource parameter
|
||||||
|
7. **Authorization Code Exchange**: After user authorization, exchange code for access token (with resource parameter)
|
||||||
|
8. **Token Usage**: Use access token for API calls via `AuthClient` or `AuthorizedHttpClient`
|
||||||
|
9. **Token Refresh**: Automatically use refresh token to get new access token when current one expires
|
||||||
|
10. **Scope Upgrade**: On 403 insufficient_scope, compute scope union and re-authorize with upgraded scopes
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
- All tokens are securely stored in memory
|
- **PKCE S256 always enforced**: never falls back to `plain` or no challenge. OAuth 2.1 mandates S256 as Mandatory To Implement for servers.
|
||||||
- PKCE implementation prevents authorization code interception attacks
|
- **RFC 8707 resource binding**: authorization and token requests include the `resource` parameter to bind tokens to the protected resource
|
||||||
- Automatic token refresh support reduces user intervention
|
- All tokens are securely stored in memory (custom credential stores supported)
|
||||||
- Only accepts HTTPS connections or secure local callback URIs
|
- Automatic token refresh reduces user intervention
|
||||||
|
- Server metadata validation warns on non-compliant configurations but proceeds where relatively safe
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|
@ -114,10 +146,15 @@ If you encounter authorization issues, check the following:
|
||||||
2. Verify callback URI matches server's allowed redirect URIs
|
2. Verify callback URI matches server's allowed redirect URIs
|
||||||
3. Check network connection and firewall settings
|
3. Check network connection and firewall settings
|
||||||
4. Verify server supports metadata discovery or dynamic client registration
|
4. Verify server supports metadata discovery or dynamic client registration
|
||||||
|
5. If PKCE fails, the server may not support S256 (non-compliant with OAuth 2.1)
|
||||||
|
6. Check `tracing` logs at debug level for detailed discovery and validation info
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization/)
|
- [MCP Authorization Specification (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization/)
|
||||||
- [OAuth 2.1 Specification Draft](https://oauth.net/2.1/)
|
- [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 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)
|
- [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591)
|
||||||
|
- [RFC 8707: Resource Indicators for OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc8707)
|
||||||
|
- [RFC 9728: OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
|
||||||
|
- [RFC 7636: Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636)
|
||||||
|
|
|
||||||
|
|
@ -114,14 +114,16 @@ async fn main() -> Result<()> {
|
||||||
client_metadata_url
|
client_metadata_url
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize oauth state machine
|
// initialize oauth state machine
|
||||||
let mut oauth_state = OAuthState::new(&server_url, None)
|
let mut oauth_state = OAuthState::new(&server_url, None)
|
||||||
.await
|
.await
|
||||||
.context("Failed to initialize oauth state machine")?;
|
.context("Failed to initialize oauth state machine")?;
|
||||||
// Use CIMD (SEP-991) with client metadata URL
|
// use CIMD (SEP-991) with client metadata URL.
|
||||||
|
// passing empty scopes lets the SDK auto-select from the server's
|
||||||
|
// WWW-Authenticate header, Protected Resource Metadata, or AS metadata.
|
||||||
oauth_state
|
oauth_state
|
||||||
.start_authorization_with_metadata_url(
|
.start_authorization_with_metadata_url(
|
||||||
&["mcp", "profile", "email"],
|
&[],
|
||||||
MCP_REDIRECT_URI,
|
MCP_REDIRECT_URI,
|
||||||
Some("Test MCP Client"),
|
Some("Test MCP Client"),
|
||||||
Some(&client_metadata_url),
|
Some(&client_metadata_url),
|
||||||
|
|
|
||||||
|
|
@ -520,16 +520,13 @@ async fn oauth_authorization_server() -> impl IntoResponse {
|
||||||
"response_types_supported".into(),
|
"response_types_supported".into(),
|
||||||
Value::Array(vec![Value::String("code".into())]),
|
Value::Array(vec![Value::String("code".into())]),
|
||||||
);
|
);
|
||||||
additional_fields.insert(
|
|
||||||
"code_challenge_methods_supported".into(),
|
|
||||||
Value::Array(vec![Value::String("S256".into())]),
|
|
||||||
);
|
|
||||||
let metadata = AuthorizationMetadata {
|
let metadata = AuthorizationMetadata {
|
||||||
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
|
authorization_endpoint: format!("http://{}/oauth/authorize", BIND_ADDRESS),
|
||||||
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
|
token_endpoint: format!("http://{}/oauth/token", BIND_ADDRESS),
|
||||||
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
|
scopes_supported: Some(vec!["profile".to_string(), "email".to_string()]),
|
||||||
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
|
registration_endpoint: Some(format!("http://{}/oauth/register", BIND_ADDRESS)),
|
||||||
response_types_supported: Some(vec!["code".to_string()]),
|
response_types_supported: Some(vec!["code".to_string()]),
|
||||||
|
code_challenge_methods_supported: Some(vec!["S256".to_string()]),
|
||||||
issuer: Some(BIND_ADDRESS.to_string()),
|
issuer: Some(BIND_ADDRESS.to_string()),
|
||||||
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
|
jwks_uri: Some(format!("http://{}/oauth/jwks", BIND_ADDRESS)),
|
||||||
additional_fields,
|
additional_fields,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue