rust_mcp_sdk/auth/spec/
discovery.rs

1use crate::{
2    auth::{OauthEndpoint, OAUTH_PROTECTED_RESOURCE_BASE, WELL_KNOWN_OAUTH_AUTHORIZATION_SERVER},
3    error::McpSdkError,
4    mcp_http::url_base,
5};
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use url::Url;
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
12pub struct AuthorizationServerMetadata {
13    /// The base URL of the authorization server (e.g., "http://localhost:8080/realms/master/").
14    pub issuer: Url,
15
16    /// URL to which the client redirects the user for authorization.
17    pub authorization_endpoint: Url,
18
19    /// URL to exchange authorization codes for tokens or refresh tokens.
20    pub token_endpoint: Url,
21
22    /// URL of the authorization server's JWK Set `JWK` document
23    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
24    pub jwks_uri: Option<Url>,
25
26    /// Endpoint where clients can register dynamically.
27    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
28    pub registration_endpoint: Option<Url>,
29
30    /// List of supported OAuth scopes (e.g., "openid", "profile", "email", mcp:tools)
31    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
32    pub scopes_supported: Option<Vec<String>>,
33
34    ///  Response Types. Required by spec. If missing, default is empty vec.
35    /// Examples: "code", "token", "id_token"
36    #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
37    pub response_types_supported: Vec<String>,
38
39    ///  Response Modes. Indicates how the authorization response is returned.
40    /// Examples: "query", "fragment", "form_post"
41    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
42    pub response_modes_supported: Option<Vec<String>>,
43
44    // ui_locales_supported
45    // op_policy_uri
46    // op_tos_uri
47    /// List of supported Grant Types
48    /// Examples: "authorization_code", "client_credentials", "refresh_token"
49    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
50    pub grant_types_supported: Option<Vec<String>>,
51
52    /// Methods like "client_secret_basic", "client_secret_post"
53    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
54    pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
55
56    /// Signing algorithms for client authentication (e.g., "RS256")
57    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
58    pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
59
60    /// Link to human-readable docs for developers.
61    /// <https://datatracker.ietf.org/doc/html/rfc8414>
62    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
63    pub service_documentation: Option<Url>,
64
65    /// OAuth 2.0 Token Revocation endpoint.
66    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
67    pub revocation_endpoint: Option<Url>,
68
69    /// Similar to token endpoint, but for revocation-specific auth.
70    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
71    pub revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
72
73    /// Tells the client which authentication methods are supported when accessing the token revocation endpoint.
74    /// These are standardized methods from RFC 6749 (OAuth 2.0)
75    /// Common values: "client_secret_basic", "client_secret_post", "private_key_jwt"
76    /// `client_secret_basic` – client credentials sent in HTTP Basic Auth.
77    /// `client_secret_post` – client credentials sent in the POST body.
78    /// `private_key_jwt` – client authenticates using a signed JWT.
79    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
80    pub revocation_endpoint_auth_methods_supported: Option<Vec<String>>,
81
82    /// URL to validate tokens and get their metadata.
83    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
84    pub introspection_endpoint: Option<Url>,
85
86    /// Auth methods for accessing introspection.
87    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
88    pub introspection_endpoint_auth_methods_supported: Option<Vec<String>>,
89
90    /// Algorithms for accessing introspection.
91    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
92    pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
93
94    /// Methods supported for PKCE (Proof Key for Code Exchange).
95    /// Common values: "plain", "S256"
96    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
97    pub code_challenge_methods_supported: Option<Vec<String>>,
98
99    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
100    pub userinfo_endpoint: Option<String>,
101}
102
103impl AuthorizationServerMetadata {
104    /// Creates a new `AuthorizationServerMetadata` instance with the minimal required fields.
105    /// According to the OAuth 2.0 Authorization Server Metadata Metadata specification (RFC 8414),
106    /// the following fields are **required** for a valid metadata document:
107    /// - `issuer`
108    /// - `authorization_endpoint`
109    /// - `token_endpoint`
110    ///
111    /// All other fields are initialized with their default values (typically `None` or empty collections).
112    ///
113    pub fn new(
114        issuer: &str,
115        authorization_endpoint: &str,
116        token_endpoint: &str,
117    ) -> Result<Self, url::ParseError> {
118        let issuer = Url::parse(issuer)?;
119        let authorization_endpoint = Url::parse(authorization_endpoint)?;
120        let token_endpoint = Url::parse(token_endpoint)?;
121
122        Ok(Self {
123            issuer,
124            authorization_endpoint,
125            token_endpoint,
126            jwks_uri: Default::default(),
127            registration_endpoint: Default::default(),
128            scopes_supported: Default::default(),
129            response_types_supported: Default::default(),
130            response_modes_supported: Default::default(),
131            grant_types_supported: Default::default(),
132            token_endpoint_auth_methods_supported: Default::default(),
133            token_endpoint_auth_signing_alg_values_supported: Default::default(),
134            service_documentation: Default::default(),
135            revocation_endpoint: Default::default(),
136            revocation_endpoint_auth_signing_alg_values_supported: Default::default(),
137            revocation_endpoint_auth_methods_supported: Default::default(),
138            introspection_endpoint: Default::default(),
139            introspection_endpoint_auth_methods_supported: Default::default(),
140            introspection_endpoint_auth_signing_alg_values_supported: Default::default(),
141            code_challenge_methods_supported: Default::default(),
142            userinfo_endpoint: Default::default(),
143        })
144    }
145
146    /// Fetches authorization server metadata from a remote `.well-known/openid-configuration`
147    /// or OAuth 2.0 Authorization Server Metadata endpoint.
148    ///
149    /// This performs an HTTP GET request and deserializes the response directly into
150    /// `AuthorizationServerMetadata`. The endpoint must return a JSON document conforming
151    /// to RFC 8414 (OAuth 2.0 Authorization Server Metadata) or OpenID Connect Discovery 1.0.
152    ///
153    pub async fn from_discovery_url(discovery_url: &str) -> Result<Self, McpSdkError> {
154        let client = Client::new();
155        let metadata = client
156            .get(discovery_url)
157            .send()
158            .await
159            .map_err(|err| McpSdkError::Internal {
160                description: err.to_string(),
161            })?
162            .json::<AuthorizationServerMetadata>()
163            .await
164            .map_err(|err| McpSdkError::Internal {
165                description: err.to_string(),
166            })?;
167        Ok(metadata)
168    }
169}
170
171/// represents metadata about a protected resource in the OAuth 2.0 ecosystem.
172/// It allows clients and authorization servers to discover how to interact with a protected resource (like an MCP endpoint),
173/// including security requirements and supported features.
174/// <https://datatracker.ietf.org/doc/rfc9728>
175#[derive(Debug, Serialize, Deserialize, Clone)]
176pub struct OauthProtectedResourceMetadata {
177    /// The base identifier of the protected resource (e.g., an MCP server's URI).
178    /// This is the only required field.
179    pub resource: Url,
180
181    /// List of authorization servers that can issue access tokens for this resource.
182    /// Allows dynamic trust discovery.
183    #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
184    pub authorization_servers: Vec<Url>,
185
186    /// URL where the resource exposes its public keys (JWKS) to verify signed tokens.
187    /// Typically used to verify JWT access tokens.
188    /// Example: `https://example.com/.well-known/jwks.json`
189    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
190    pub jwks_uri: Option<Url>,
191
192    /// OAuth scopes the resource supports (e.g., "mcp:tool", "read", "write", "admin").
193    /// Helps clients know what they can request for access.
194    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
195    pub scopes_supported: Option<Vec<String>>,
196
197    /// Methods accepted for presenting Bearer tokens:
198    /// `authorization_header` (typical)
199    /// `form_post`
200    /// `uri_query`
201    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
202    pub bearer_methods_supported: Option<Vec<String>>,
203
204    /// Supported signing algorithms for access tokens (if tokens are JWTs).
205    /// Example: ["RS256", "ES256"]
206    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
207    pub resource_signing_alg_values_supported: Option<Vec<String>>,
208
209    /// A human-readable name for the resource.
210    /// Useful for UIs, logs, or developer documentation.
211    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
212    pub resource_name: Option<String>,
213
214    /// URL to developer docs describing the resource and how to use it.
215    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
216    pub resource_documentation: Option<String>,
217
218    /// URL to the resource's access policy or terms (e.g., rules on who can access what).
219    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
220    pub resource_policy_uri: Option<Url>,
221
222    /// URL to terms of service applicable to this resource.
223    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
224    pub resource_tos_uri: Option<Url>,
225
226    /// If true, access tokens must be bound to a client TLS certificate.
227    /// Used in mutual TLS scenarios for additional security.
228    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
229    pub tls_client_certificate_bound_access_tokens: Option<bool>,
230
231    ///Lists structured authorization types supported (used with Rich Authorization Requests (RAR)
232    /// Example: ["payment_initiation", "account_information"]
233    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
234    pub authorization_details_types_supported: Option<Vec<String>>,
235
236    /// Supported algorithms for DPoP (Demonstration of Proof-of-Possession) tokens.
237    /// Example: ["ES256", "RS256"]
238    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
239    pub dpop_signing_alg_values_supported: Option<Vec<String>>,
240
241    /// If true, the resource requires access tokens to be DPoP-bound.
242    /// Enhances security by tying tokens to a specific client and key.
243    #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
244    pub dpop_bound_access_tokens_required: Option<bool>,
245}
246
247impl OauthProtectedResourceMetadata {
248    /// Creates a new `OAuthProtectedResourceMetadata` instance with only the
249    /// minimal required fields populated.
250    ///
251    /// The `resource` and each entry in `authorization_servers` must be valid URLs.
252    /// All other metadata fields are initialized to their defaults.
253    /// To provide optional or extended metadata, assign those fields after creation or construct the struct directly.
254    pub fn new<S>(
255        resource: S,
256        authorization_servers: Vec<S>,
257        scopes_supported: Option<Vec<String>>,
258    ) -> Result<Self, url::ParseError>
259    where
260        S: AsRef<str>,
261    {
262        let resource = Url::parse(resource.as_ref())?;
263        let authorization_servers: Vec<_> = authorization_servers
264            .iter()
265            .map(|s| Url::parse(s.as_ref()))
266            .collect::<Result<_, _>>()?;
267
268        Ok(Self {
269            resource,
270            authorization_servers,
271            jwks_uri: Default::default(),
272            scopes_supported,
273            bearer_methods_supported: Default::default(),
274            resource_signing_alg_values_supported: Default::default(),
275            resource_name: Default::default(),
276            resource_documentation: Default::default(),
277            resource_policy_uri: Default::default(),
278            resource_tos_uri: Default::default(),
279            tls_client_certificate_bound_access_tokens: Default::default(),
280            authorization_details_types_supported: Default::default(),
281            dpop_signing_alg_values_supported: Default::default(),
282            dpop_bound_access_tokens_required: Default::default(),
283        })
284    }
285}
286
287pub fn create_protected_resource_metadata_url(path: &str) -> String {
288    format!(
289        "{OAUTH_PROTECTED_RESOURCE_BASE}{}",
290        if path == "/" { "" } else { path }
291    )
292}
293
294pub fn create_discovery_endpoints(
295    mcp_server_url: &str,
296) -> Result<(HashMap<String, OauthEndpoint>, String), McpSdkError> {
297    let mut endpoint_map = HashMap::new();
298    endpoint_map.insert(
299        WELL_KNOWN_OAUTH_AUTHORIZATION_SERVER.to_string(),
300        OauthEndpoint::AuthorizationServerMetadata,
301    );
302
303    let resource_url = Url::parse(mcp_server_url).map_err(|err| McpSdkError::Internal {
304        description: err.to_string(),
305    })?;
306
307    let relative_url = create_protected_resource_metadata_url(resource_url.path());
308    let base_url = url_base(&resource_url);
309    let protected_resource_metadata_url =
310        format!("{}{relative_url}", base_url.trim_end_matches('/'));
311
312    endpoint_map.insert(relative_url, OauthEndpoint::ProtectedResourceMetadata);
313
314    Ok((endpoint_map, protected_resource_metadata_url))
315}