* fix(auth): validate discovered metadata issuer
Validate authorization server metadata issuers against the issuer implied by
standard discovery URLs before trusting advertised endpoints.
fixes#983
* test(auth): add required issuer to authorization-server mock metadata
---------
Co-authored-by: Alex Hancock <alexhancock@block.xyz>
SEP-2350 [1] clarifies that scope accumulation is a client-side
responsibility: during re-authorization the client requests the union of
its previously requested scopes and the newly challenged scopes, because
servers report only the scopes needed for the current operation in
403 / insufficient_scope challenges (RFC 6750 §3.1), not the union of
everything granted so far.
select_base_scopes returned a single source, so a 403 challenge replaced
the previously requested scopes instead of widening them, dropping prior
permissions across step-up rounds. I make it union the previously
requested scopes, the WWW-Authenticate challenge, and the protected
resource metadata scopes (RFC 9728), treating each server-reported set as
an operational requirement for the current operation rather than an
exclusive directive; AS metadata and caller defaults only seed the
request when nothing has been requested or challenged yet.
exchange_code_for_token treats an explicit scope list as authoritative,
so a server may still narrow the grant. When the server omits scope it
has granted exactly what the client requested (RFC 6749 §5.1), so the
grant has to fall back to the scopes requested in this round, not the
previously granted set; otherwise a step-up that the server confirms by
omitting scope would silently drop the just-added permission and the
client would loop on the same 403. The widened request was not persisted
anywhere the exchange could read it, so I record it on
StoredAuthorizationState per authorization (defaulting empty for states
stored before this field existed) and resolve the grant from there. This
addresses review feedback [2] that the earlier fallback returned the
previous grant rather than the request. Deduplication preserves
first-seen order for stable, testable output. Tests cover multi-round
accumulation, dedup, resource-metadata unioning, and grant resolution
when the response omits scope. Implements [3].
[1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/authorization.mdx#L682
[2]: https://github.com/modelcontextprotocol/rust-sdk/pull/888#pullrequestreview-4454854582
[3]: https://github.com/modelcontextprotocol/rust-sdk/issues/877
Signed-off-by: Stefano Amorelli <stefano@amorelli.tech>
* fix(auth): support oauth metadata fallbacks
* ci: run client conformance scenarios
* fix: pass full client conformance suite
* ci: run full client conformance suite
* fix: update SSE stream constructor
* fix: interpret task ttl as milliseconds
* fix: use text/plain for default text mime type
* fix: include resource param in token refresh
* test: align conformance prompt args with runner
* ci: run server conformance suite on PRs
* ci: build client bin and gate pending scenarios
Per RFC 6749 section 6 and OAuth 2.1: when the server does not issue a new
refresh_token on a refresh response, the client MUST keep the existing one.
AuthorizationManager::refresh_token() was persisting the response verbatim,
dropping the previous refresh_token and forcing full re-authorization on
the next refresh.
Match the fix from python-sdk#2270: preserve the existing refresh_token
when the response omits it, replace when the server rotates.
Fixes#921
Signed-off-by: SarthakB11 <sarthak.bhardwaj21b@iiitg.ac.in>
* feat(auth): specify OIDC application_type during client registration
SEP-837 [1] requires an MCP client to specify an application_type during
OIDC Dynamic Client Registration. When it is omitted, OIDC servers
default the client to "web", which conflicts with the loopback redirect
URIs that CLI and desktop clients use, so the registration can be
rejected.
I make register_client always send an application_type. It defaults to
"native" to match the loopback redirect this SDK uses, and I added
OAuthClientConfig::with_application_type so web clients can opt in. Tests
cover the serialized request body and the config default. Implements [2].
[1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/authorization.mdx#L395
[2]: https://github.com/modelcontextprotocol/rust-sdk/issues/880
Signed-off-by: Stefano Amorelli <stefano@amorelli.tech>
* chore(auth): declare application_type in client metadata document
I set application_type to "native" in the hosted client metadata
document so the URL-based client id flow and dynamic registration agree
on the client type that SEP-837 [1] expects.
[1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/authorization.mdx#L395
Signed-off-by: Stefano Amorelli <stefano@amorelli.tech>
---------
Signed-off-by: Stefano Amorelli <stefano@amorelli.tech>
In fetch_resource_metadata_from_url, a JSON parse failure on the
response body caused a fatal AuthError::MetadataError, preventing
discover_metadata() from falling through to direct
.well-known/oauth-authorization-server discovery (Strategy B).
MCP servers that return HTTP 200 with non-JSON content (e.g. HTML)
at their base URL caused the OAuth flow to abort entirely, even
when the server had a valid .well-known/oauth-authorization-server
endpoint.
Return Ok(None) on parse failure, consistent with how HTTP errors
are already handled in the same function.
StoredCredentials is #[non_exhaustive] but has no constructor, making
it impossible for external crates implementing CredentialStore to
construct instances without a serde roundtrip workaround. Add a new()
constructor matching the pattern used for other #[non_exhaustive]
types in this crate.
Fixes#777
* fix(auth): redact secrets in Debug output for StoredCredentials and StoredAuthorizationState
Removes `Debug` from the derive macros on `StoredCredentials` and
`StoredAuthorizationState` and replaces them with manual `Debug` impls
that print `[REDACTED]` for sensitive fields (access/refresh tokens,
PKCE verifiers, and CSRF tokens), preventing accidental credential
leakage via `{:?}` formatters, log calls, and error chains.
Fixes#741
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(auth): assert Debug output redacts secrets for credential types
Adds regression tests for the fix in the previous commit, verifying
that `{:?}` formatting of `StoredAuthorizationState` and
`StoredCredentials` does not emit plaintext secrets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(auth): address review feedback on debug redaction tests
- Remove redundant VendorExtraTokenFields from use super:: in
test_stored_credentials_debug_redacts_token_response (already
imported at module scope)
- Add assert!(debug_output.contains("created_at")) to
test_stored_authorization_state_debug_redacts_secrets to verify
non-secret fields remain visible in Debug output
- Run cargo fmt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update crates/rmcp/src/transport/auth.rs
Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>
* fix: remaining formatting issue
* fix: formatting
* fix: formatting
* fix: please
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>
* fix(auth): pass WWW-Authenticate scopes to DCR registration request
When an MCP server returns a 401 with `WWW-Authenticate: Bearer scope="..."`,
the scopes are parsed but never included in the Dynamic Client Registration
(DCR) request. Per RFC 7591, the DCR request should include a `scope` field
so the authorization server knows what scopes the client intends to use.
Servers that enforce scope-matching between registration and authorization
will reject the flow without this.
Changes:
- Add optional `scope` field to `ClientRegistrationRequest` with
`skip_serializing_if` for backward compatibility
- Update `register_client()` to accept scopes parameter and include
them in the DCR request body and returned `OAuthClientConfig`
- Thread scopes from `AuthorizationSession::new()` into both
`register_client()` call sites
- Re-export `oauth2::TokenResponse` trait so consumers can extract
scopes from token responses
- Add serialization tests for the new `scope` field
* refactor(auth): change register_client to accept &[&str] instead of &[String]
Avoids unnecessary Vec<String> allocation in callers that already have &[&str].
* fix(auth): make ClientRegistrationRequest crate-private
* refactor(auth): stop re-exporting oauth2 TokenResponse trait
* style(auth): merge TokenResponse into grouped oauth2 import
Fix nightly rustfmt check by consolidating the separate
`use oauth2::TokenResponse` into the existing `use oauth2::{...}` block.
* feat(auth): support returning extra fields that may be returned from token generation
exchange_code_for_token and refresh_token now return a StandardTokenResponse which includes
any additionalfields which might have been sent by the vendor
BREAKING CHANGE: Return type of exchange_code_for_token and refresh_token has changed
and may require code changes.
* fix: doc links
* feat: add support for custom HTTP headers in StreamableHttpClient
* feat: implement reserved header checks for custom HTTP headers in StreamableHttpClient
* feat(auth): add token_endpoint_auth_method to OAuthClientConfig
Some OAuth providers (e.g. HubSpot) require client credentials to be
sent as POST body parameters (client_secret_post) instead of via HTTP
Basic Auth header. The oauth2 crate defaults to BasicAuth, and rmcp
had no way to override this, causing TokenExchangeFailed errors.
Add an optional `token_endpoint_auth_method` field to OAuthClientConfig
that accepts "client_secret_post" (RequestBody) and "client_secret_basic"
(BasicAuth). Unknown values are silently ignored, preserving the default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(auth): derive token_endpoint_auth_method from server metadata
Move auth method selection from per-client config to server's
AuthorizationMetadata, which is the correct OAuth 2.0 approach.
Servers like HubSpot advertise token_endpoint_auth_methods_supported
in their metadata; reading it from there avoids manual configuration
and prevents TokenExchangeFailed errors with non-BasicAuth providers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(auth): read token_endpoint_auth_methods_supported from additional_fields
Move token_endpoint_auth_methods_supported out of AuthorizationMetadata
as an explicit field and read it from the serde(flatten) additional_fields
HashMap instead. This avoids serializing `null` when the field is absent,
which broke Zod validation in downstream consumers like MCP Inspector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(auth): prefer basic auth when both methods supported and improve test assertions
When token_endpoint_auth_methods_supported contains both client_secret_post
and client_secret_basic, default to basic auth per RFC 6749 §2.3.1.
Update configure_client tests to assert actual AuthType instead of is_some().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(auth): apply cargo fmt formatting
* style(auth): apply nightly cargo fmt import grouping
* revert: undo .gitignore change
---------
Co-authored-by: Anar Azadaliyev <anar.azadaliye@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add OpenID Connect discovery support per spec-2025-11-25 4.3
Previously only tried OAuth 2.0 endpoints. Now tries OAuth first, then
OpenID Connect Discovery 1.0 in the spec-mandated priority order.
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* fix: format auth.rs test assertions
Reformat assert_eq! statements to satisfy rustfmt checks in CI.
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
---------
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* feat(auth): add cimd support for SEP-991
add cimd support for url-based client ids
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* test(auth): add unit tests for is_https_url helper
Add test coverage for is_https_url helper to validate HTTPS scheme, non-root paths,
and reject http, javascript, data schemes, and invalid inputs per SEP-991 requirements.
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* feat(example): add CIMD OAuth server for SEP-991 testing
Implements a new server example (servers_cimd_auth_streamhttp) that
demonstrates CIMD (Client ID Metadata Document) support for URL-based
client IDs. The server validates client_id URLs, fetches and validates
client metadata documents, and provides OAuth 2.0 authorization endpoints
with MCP integration for end-to-end testing.
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* fix(oauth): add CORS headers to token endpoint
Add CORS headers to token endpoint to allow cross-origin requests from browsers
during OAuth authorization code exchange flow.
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* refactor: improve is_https_url function and consolidate tests
- Improve is_https_url function formatting and readability
- Merge all test cases into single test_is_https_url_scenarios function
- Add missing test case for "https://" URL
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* refactor: use map_err instead of match for error handling in auth.rs
Replace the verbose match statement with
map_err for more idiomatic
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
* feat: add client-metadata.json
Add client metadata file for SEP-991 CIMD
authentication support
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
---------
Signed-off-by: tanish111 <tanishdesai37@gmail.com>
SSE transport has been removed from the MCP specification in favor of
streamable HTTP. This removes all SSE-specific transport code:
- Remove `transport-sse-client` and `transport-sse-server` features
- Remove `SseClientTransport` and `SseServer` types
- Remove SSE-specific examples (`counter_sse`, `counter_sse_directly`)
- Migrate auth examples from SSE to streamable HTTP
- Update tests to remove SSE transport usage
- Update documentation
BREAKING CHANGE: The following have been removed:
- `transport-sse-client` feature
- `transport-sse-client-reqwest` feature
- `transport-sse-server` feature
- `SseClientTransport` type
- `SseServer` type
- `sse_client` and `sse_server` modules
Users should migrate to streamable HTTP transport which provides
equivalent functionality. See `StreamableHttpClientTransport` and
`StreamableHttpService` for the replacement APIs.
Ref: https://github.com/modelcontextprotocol/rust-sdk/pull/561#issuecomment-3576551699
auth.rs was using an in-memory expires-at which is only set on initial token exchange.
Instead, this PR switches it to use the expires-at set in the credentials that are passed in.
Many MCP Servers use client_name for a variety of things including:
* Whitelisting
* Logos
* Copy shown directly on the page
* etc
As a result, it's important for MCP Clients to be able to override the client name.