Let a provider add parameters to the token requests it makes
## Motivation and Context `ClientCredentialsProvider` builds a fixed `client_credentials` token request: `grant_type`, `scope`, `resource`, and the client authentication that `Flow#post_to_token_endpoint` adds. An authorization server that requires further parameters, such as Auth0's `audience`, cannot be used through `oauth:` at all, and giving up `oauth:` also gives up the bearer header and the 401-driven retry that the transport provides. RFC 6749 Section 8.2 allows extension parameters on the token request, and the TypeScript SDK lets a provider shape its token request through `prepareTokenRequest`. `ClientCredentialsProvider.new` now takes `token_request_params:`, a Hash of String keys and values added to every token request the provider makes, and `Flow#post_to_token_endpoint` reads the same name by duck typing, so a `Provider` or `CrossAppAccessProvider` subclass that defines the method gets the same treatment on its authorization code exchange, refresh, or `jwt-bearer` request. `Provider.new` and `CrossAppAccessProvider.new` take the same keyword, held by `StorageBackedProvider` like `authorization_request_validator`, so every bundled provider refuses a bad value before anything is stored. The parameters go underneath the flow's own, so the SDK's values always win, and a key the SDK sets itself (`Flow::RESERVED_TOKEN_REQUEST_PARAMS`) is refused rather than silently overridden: with `ArgumentError` from the constructor, before any `client_information` is written, and again with `ArgumentError` from the flow, before the token request is sent. Only Strings are accepted because `URI.encode_www_form` encodes other scalars and Arrays in ways the caller did not write, and the provider keeps a frozen copy so a later change to the caller's Hash cannot alter what is sent. A Hash comparing keys by identity is refused too, since two equal keys are two entries there, sent twice by a provider method or silently collapsed into one by the copy. The flow raises `ArgumentError` rather than `AuthorizationError` because `MCP::Client::HTTP` treats a failed refresh as a reason to run the interactive flow, which would then fail the same way after the user signed in. Both refusals raise `Flow::InvalidTokenRequestParamsError`, a subclass of `ArgumentError`, so a caller can tell them from an unknown keyword, and the copy duplicates keys as well as values, since `Hash` copies only keys whose class is exactly `String`. Fixes #554. ## How Has This Been Tested? New tests cover every token request the parameters can ride (the `client_credentials` grant under all three client authentication methods, the authorization code exchange, refresh, and `jwt-bearer`), both refusal boundaries (the constructors of all three providers, and the flow for a provider defining the method), and the transport surfacing a refused hook from a refresh attempt without starting the interactive flow. All of them fail against the previous library. ## Breaking Changes None. The keyword is optional and the hook is opt-in; a provider that does not define `token_request_params` sends the same requests as before.
This commit is contained in:
parent
41fbf05d26
commit
8ad80dd0b4
1 changed files with 583 additions and 24 deletions
|
|
@ -112,6 +112,14 @@ Optional keyword arguments:
|
|||
served at the URL is a separate JSON artifact from the `client_metadata` keyword above:
|
||||
the DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include
|
||||
`client_id` set to the document URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
|
||||
- `token_request_params`: Hash of String keys and values added to every token request the provider makes,
|
||||
for parameters the authorization server requires beyond the grant itself, such as Auth0's `audience`.
|
||||
Defaults to `nil`, which adds nothing. The authorization request is not affected.
|
||||
A key the SDK sets itself (listed in `Flow::RESERVED_TOKEN_REQUEST_PARAMS`), a Hash that compares keys by identity,
|
||||
or a value that is not a Hash of Strings is refused with `Flow::InvalidTokenRequestParamsError`, a subclass of `ArgumentError`,
|
||||
when the provider is built, and the accepted Hash is copied and frozen.
|
||||
Any provider that defines a `token_request_params` method gets the same treatment on every token request it makes;
|
||||
the flow checks the returned value by the same rules and raises the same error before the token request is sent.
|
||||
|
||||
{: .warning }
|
||||
> The OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591) is deprecated as a client registration mechanism as of MCP 2026-07-28 in favor of Client ID Metadata Documents,
|
||||
|
|
@ -192,6 +200,7 @@ provider = MCP::Client::OAuth::ClientCredentialsProvider.new(
|
|||
client_secret: ENV.fetch("MCP_CLIENT_SECRET"),
|
||||
# token_endpoint_auth_method: "client_secret_basic" (default), "client_secret_post", or "private_key_jwt"
|
||||
# scope: "mcp:read mcp:write" (optional; used when the server does not advertise scopes)
|
||||
# token_request_params: { "audience" => "https://api.example.com" } (optional; parameters the authorization server requires)
|
||||
)
|
||||
|
||||
transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp", oauth: provider)
|
||||
|
|
@ -207,7 +216,8 @@ Keyword arguments:
|
|||
- `private_key`, `signing_algorithm`: Required with `private_key_jwt` - the key (a PEM string
|
||||
or `OpenSSL::PKey::PKey`, never written to `storage`) signs the client assertion with `"ES256"`
|
||||
or `"RS256"`; `client_secret` must not be set, because the private key is the credential.
|
||||
- `scope`, `storage`, `authorization_request_validator`: Optional, same meaning as on `Provider`.
|
||||
- `scope`, `storage`, `authorization_request_validator`, `token_request_params`: Optional, same meaning as on `Provider`.
|
||||
Use `token_request_params` for a parameter the authorization server requires on the `client_credentials` grant, such as Auth0's `audience`.
|
||||
|
||||
### Cross-App Access (JWT Bearer) Grant
|
||||
|
||||
|
|
@ -244,7 +254,7 @@ Keyword arguments:
|
|||
- `assertion_provider`: Required. Callable invoked as `call(audience:, resource:)` and returning the ID-JAG assertion.
|
||||
`audience` is the MCP authorization server's validated issuer identifier; `resource` is the canonical MCP server URL (RFC 8707).
|
||||
Passing both through to `IDJAGTokenExchange.request` covers the common case.
|
||||
- `scope`, `storage`, `authorization_request_validator`: Optional, same meaning as on `Provider`.
|
||||
- `scope`, `storage`, `authorization_request_validator`, `token_request_params`: Optional, same meaning as on `Provider`.
|
||||
|
||||
### Communication Security
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue