@@ -997,9 +997,9 @@ def exchange_refresh_token(as_metadata:, client_info:, refresh_token:, resource:
997997 post_to_token_endpoint ( as_metadata : as_metadata , client_info : client_info , form : form )
998998 end
999999
1000- # Submits a form-encoded request to the token endpoint, applying
1001- # the client authentication method advertised in `client_information` and
1002- # adding `client_id` (and `client_secret` when not using HTTP Basic) .
1000+ # Submits a form-encoded token request using the authentication method
1001+ # stored in `client_information`. The method determines whether client
1002+ # credentials belong in the form body, a Basic header, or a JWT assertion .
10031003 def post_to_token_endpoint ( as_metadata :, client_info :, form :)
10041004 client_id = client_info_required_value ( client_info , "client_id" )
10051005 unless client_id
@@ -1010,12 +1010,13 @@ def post_to_token_endpoint(as_metadata:, client_info:, form:)
10101010 client_secret = client_info_required_value ( client_info , "client_secret" )
10111011 token_endpoint_auth_method = client_info_value ( client_info , "token_endpoint_auth_method" )
10121012
1013- form = if token_endpoint_auth_method == "private_key_jwt"
1014- # RFC 7523 Section 2.2 JWT client assertion for the `private_key_jwt` method of
1015- # the `io.modelcontextprotocol/oauth-client-credentials` extension (SEP-1046).
1016- # The client identity travels in the assertion's `iss`/`sub` claims, so `client_id` is
1017- # omitted from the body per RFC 7521 Section 4.2 (the `client_assertion` conveys the client identity).
1018- # The audience is the issuer identifier that `ensure_issuer_matches!` already byte-validated.
1013+ # Apply one client authentication method per request (RFC 6749 Section 2.3).
1014+ headers = { }
1015+ form = case token_endpoint_auth_method
1016+ when "private_key_jwt"
1017+ # The assertion identifies the client through its `iss` and `sub`
1018+ # claims, so the body needs no separate `client_id` (RFC 7521 Section 4.2).
1019+ # Use the issuer already checked by `ensure_issuer_matches!` as the audience.
10191020 unless @provider . respond_to? ( :client_assertion )
10201021 raise AuthorizationError ,
10211022 "token_endpoint_auth_method is private_key_jwt but the provider does not " \
@@ -1026,22 +1027,24 @@ def post_to_token_endpoint(as_metadata:, client_info:, form:)
10261027 "client_assertion_type" => JWTClientAssertion ::ASSERTION_TYPE ,
10271028 "client_assertion" => @provider . client_assertion ( audience : as_metadata [ "issuer" ] ) ,
10281029 )
1030+ when "client_secret_post"
1031+ # Send the client ID and available secret in the form body.
1032+ body = form . merge ( "client_id" => client_id )
1033+ body [ "client_secret" ] = client_secret if client_secret
1034+ body
10291035 else
1030- form . merge ( "client_id" => client_id )
1031- end
1032-
1033- headers = { }
1034- if client_secret
1035- case token_endpoint_auth_method
1036- when "client_secret_post"
1037- form [ "client_secret" ] = client_secret
1038- when "none"
1039- # Public client; no credential.
1040- else
1041- # RFC 6749 §2.3.1 recommends Basic for confidential clients and
1042- # both Python and TypeScript SDKs default here when
1043- # the authentication method is not explicitly stored.
1036+ if client_secret && token_endpoint_auth_method != "none"
1037+ # Basic is also the fallback when a secret is present but no method
1038+ # is stored. A body `client_id` is optional (RFC 6749 Section 3.2.1);
1039+ # omit it because some servers treat it alongside Basic as a second
1040+ # authentication method and reject the request with `invalid_request`.
10441041 headers [ "Authorization" ] = "Basic " + basic_auth_credentials ( client_id , client_secret )
1042+ form
1043+ else
1044+ # With `none` or no secret, identify the client using `client_id`.
1045+ # This is required for unauthenticated authorization-code exchanges
1046+ # (RFC 6749 Section 3.2.1).
1047+ form . merge ( "client_id" => client_id )
10451048 end
10461049 end
10471050
0 commit comments