Skip to content

fix(auth): bind cached OAuth credentials to issuer and protected resource - #1110

Draft
tsarlandie-oai wants to merge 1 commit into
modelcontextprotocol:mainfrom
tsarlandie-oai:codex/oauth-credential-resource-binding
Draft

fix(auth): bind cached OAuth credentials to issuer and protected resource#1110
tsarlandie-oai wants to merge 1 commit into
modelcontextprotocol:mainfrom
tsarlandie-oai:codex/oauth-credential-resource-binding

Conversation

@tsarlandie-oai

Copy link
Copy Markdown
Contributor

Motivation

OAuth access and refresh tokens belong to both the authorization server that
issued them and the protected resource for which they were requested. rmcp
already sends OAuth resource indicators, but cached credentials do not retain
that resource and issuer checks are not applied consistently across every
restore, access, and refresh path.

Consequently, changing authorization servers, sharing a credential store, or
using the same authorization server with different protected resources can
reuse credentials outside their original boundary. Downstream clients should
not need to reimplement this invariant around the SDK.

What changed

  • Persist the requested protected resource alongside issuer provenance for
    authorization-code, refresh-token, and client-credentials grants.
  • Validate the stored issuer and exact protected-resource identity before
    restoring, returning, or refreshing cached tokens.
  • Add provenance-preserving
    OAuthState::{get_stored_credentials,set_stored_credentials} helpers for
    applications that import or export cached credentials.
  • Invalidate configured OAuth clients when authorization-server identity or
    endpoints change, and reject stale token endpoints or mismatched client
    identities before transmitting refresh credentials.
  • Preserve reusable client-registration information where safe while
    discarding tokens belonging to another issuer or resource.
  • Recheck the protected resource across asynchronous refresh requests and
    credential-store writes.

Backward compatibility

Existing serialized credential records remain deserializable through
#[serde(default)]. Legacy records without a resource binding are migrated
without another sign-in when both conditions hold:

  1. Their stored issuer matches verified authorization-server metadata.
  2. They contain a refresh token.

The SDK immediately refreshes the legacy credentials for the current protected
resource and exposes only the newly resource-bound access token. It never
treats a missing issuer or resource as a wildcard.

Reauthorization is required only when the issuer cannot be verified, no refresh
token exists, or the authorization server permanently rejects migration with
invalid_grant, invalid_target, or invalid_scope. Temporary failures
preserve the original credentials for retry. Migration never expands the
originally granted scopes, and failed imports preserve existing credentials and
OAuth client state.

StoredCredentials is already #[non_exhaustive], so adding its new optional
resource field does not break external struct construction;
StoredCredentials::new remains unchanged.

Regression coverage

Tests exercise issuer and resource isolation across initialization, credential
access, direct refresh, imported credentials, authorization-code exchange,
client-credentials grants, and JWT client authentication. They also cover
transparent legacy migration, absent or mismatched issuers, refresh-token
rotation, permanent versus transient refresh failures, issuer changes sharing a
token endpoint, stale OAuth clients, scope preservation, and resource changes
during both HTTP requests and asynchronous credential storage.

Validation

  • cargo test --offline -p rmcp --features auth --lib transport::auth::tests
    224 OAuth tests passed.
  • cargo test --offline -p rmcp --features auth-client-credentials-jwt --lib transport::auth::tests
    231 OAuth/JWT tests passed.
  • cargo test --offline -p rmcp --features auth,client,transport-io — package,
    integration, and documentation tests passed.
  • cargo test --offline -p rmcp --all-features --lib --quiet — 549 tests
    passed.
  • cargo clippy --offline -p rmcp --all-targets --all-features -- -D warnings.
  • rustfmt --check --edition 2024 crates/rmcp/src/transport/auth.rs.
  • git diff --check.

Breaking changes

None. Existing serialized credentials, public constructors, issuerless
authorization servers with already resource-bound credentials, and previously
supported client-registration flows remain compatible.

@github-actions github-actions Bot added T-core Core library changes T-transport Transport layer changes labels Jul 31, 2026
@tsarlandie-oai
tsarlandie-oai force-pushed the codex/oauth-credential-resource-binding branch from db186e1 to 475e45c Compare July 31, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-core Core library changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant