Skip to content

Support OAuth client_credentials grant in OAuth client#399

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:client_credentials_basic
Open

Support OAuth client_credentials grant in OAuth client#399
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:client_credentials_basic

Conversation

@koic

@koic koic commented Jun 12, 2026

Copy link
Copy Markdown
Member

Motivation and Context

The MCP authorization specification and SEP-1046 allow confidential clients to obtain tokens through the OAuth 2.1 client_credentials grant: a machine-to-machine flow with no user, no browser redirect, and no authorization code. The Ruby SDK previously supported only the interactive authorization-code flow, so it could not authenticate against a server that requires client_credentials. This makes the auth/client-credentials-basic conformance scenario pass and is removed from the expected failures list.

Following the TypeScript SDK (ClientCredentialsProvider) and the Python SDK (ClientCredentialsOAuthProvider), the grant gets its own provider class rather than overloading the authorization-code provider. The change adds:

  • MCP::Client::OAuth::ClientCredentialsProvider, a dedicated provider for the grant. client_id and client_secret are mandatory (the grant is for confidential clients), token_endpoint_auth_method is client_secret_basic (default) or client_secret_post -- none is rejected because an unauthenticated client_credentials request is meaningless. It has no redirect arguments, so a credentials-only client never supplies the redirect URI / handlers that grant has no use for.
  • MCP::Client::OAuth::StorageBackedProvider, an internal module that shares the token / client-information persistence between the two provider classes so Flow can treat any provider uniformly.
  • Provider#authorization_flow (:authorization_code) and ClientCredentialsProvider#authorization_flow (:client_credentials). Flow#run! dispatches on this rather than inspecting client_metadata[:grant_types], which is protocol metadata for the authorization server, not an SDK control signal.
  • run_client_credentials! shares the same PRM / AS metadata discovery and Communication Security checks as the interactive flow, then exchanges grant_type=client_credentials at the token endpoint via the existing post_to_token_endpoint (which already applies client_secret_basic). There is no PKCE, redirect, or offline_access augmentation, because the grant does not issue a refresh token (OAuth 2.1 Section 4.3.3).
  • conformance/client.rb builds a ClientCredentialsProvider for the auth/client-credentials-* scenarios, reusing the harness-injected client_id / client_secret.

How Has This Been Tested?

New ClientCredentialsProvider tests cover: credentials are stored as client information; authorization_flow is :client_credentials; client_secret_post is accepted; missing client_id / client_secret and token_endpoint_auth_method: "none" are rejected; and the token helpers delegate to storage.

New Flow tests cover: a client_credentials provider exchanges grant_type=client_credentials with HTTP Basic auth, saves the token, and never contacts the authorization or registration endpoint; the requested scope is taken from PRM scopes_supported; and a standard Provider runs the authorization-code grant regardless of what its client_metadata[:grant_types] lists. A Provider test asserts authorization_flow is :authorization_code.

Conformance: auth/client-credentials-basic now passes 7/7 with no failures.

Breaking Changes

None. ClientCredentialsProvider is a new class; the existing Provider keeps its required redirect_uri / redirect_handler / callback_handler arguments and its behaviour is unchanged. Existing interactive clients are unaffected.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

The MCP authorization specification and SEP-1046 allow confidential clients to
obtain tokens through the OAuth 2.1 `client_credentials` grant: a machine-to-machine flow
with no user, no browser redirect, and no authorization code. The Ruby SDK
previously supported only the interactive authorization-code flow, so it could not
authenticate against a server that requires `client_credentials`. This makes
the `auth/client-credentials-basic` conformance scenario pass and is removed from
the expected failures list.

Following the TypeScript SDK (`ClientCredentialsProvider`) and the Python SDK
(`ClientCredentialsOAuthProvider`), the grant gets its own provider class rather than
overloading the authorization-code provider. The change adds:

- `MCP::Client::OAuth::ClientCredentialsProvider`, a dedicated provider
  for the grant. `client_id` and `client_secret` are mandatory
  (the grant is for confidential clients), `token_endpoint_auth_method` is
  `client_secret_basic` (default) or `client_secret_post` -- `none` is
  rejected because an unauthenticated `client_credentials` request is
  meaningless. It has no redirect arguments, so a credentials-only
  client never supplies the redirect URI / handlers that grant has no use for.
- `MCP::Client::OAuth::StorageBackedProvider`, an internal module that
  shares the token / client-information persistence between the two provider classes
  so `Flow` can treat any provider uniformly.
- `Provider#authorization_flow` (`:authorization_code`) and
  `ClientCredentialsProvider#authorization_flow` (`:client_credentials`).
  `Flow#run!` dispatches on this rather than inspecting
  `client_metadata[:grant_types]`, which is protocol metadata for
  the authorization server, not an SDK control signal.
- `run_client_credentials!` shares the same PRM / AS metadata discovery
  and Communication Security checks as the interactive flow, then
  exchanges `grant_type=client_credentials` at the token endpoint via
  the existing `post_to_token_endpoint` (which already applies
  `client_secret_basic`). There is no PKCE, redirect, or `offline_access`
  augmentation, because the grant does not issue a refresh token
  (OAuth 2.1 Section 4.3.3).
- `conformance/client.rb` builds a `ClientCredentialsProvider` for
  the `auth/client-credentials-*` scenarios, reusing the harness-injected
  `client_id` / `client_secret`.

## How Has This Been Tested?

New `ClientCredentialsProvider` tests cover: credentials are stored as
client information; `authorization_flow` is `:client_credentials`;
`client_secret_post` is accepted; missing `client_id` / `client_secret`
and `token_endpoint_auth_method: "none"` are rejected; and the token helpers
delegate to storage.

New `Flow` tests cover: a `client_credentials` provider exchanges
`grant_type=client_credentials` with HTTP Basic auth, saves the token,
and never contacts the authorization or registration endpoint;
the requested scope is taken from PRM `scopes_supported`; and a standard
`Provider` runs the authorization-code grant regardless of what its
`client_metadata[:grant_types]` lists. A `Provider` test asserts
`authorization_flow` is `:authorization_code`.

Conformance: `auth/client-credentials-basic` now passes 7/7 with no failures.

## Breaking Changes

None. `ClientCredentialsProvider` is a new class; the existing `Provider` keeps
its required `redirect_uri` / `redirect_handler` / `callback_handler` arguments
and its behaviour is unchanged. Existing interactive clients are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant