Skip to content

fix(client/oauth): preserve token endpoint error diagnostics - #549

Open
junyuanz1 wants to merge 1 commit into
modelcontextprotocol:mainfrom
junyuanz1:fix/oauth-token-error-diagnostics
Open

fix(client/oauth): preserve token endpoint error diagnostics#549
junyuanz1 wants to merge 1 commit into
modelcontextprotocol:mainfrom
junyuanz1:fix/oauth-token-error-diagnostics

Conversation

@junyuanz1

Copy link
Copy Markdown
Contributor

Token endpoint failures currently discard the OAuth error description, leaving callers with only an HTTP status. For example, Notion's invalid_request response explaining that a client used multiple authentication methods becomes only Token endpoint returned status 400., forcing callers to replay the request to diagnose it.

Preserve RFC 6749 §5.2 error and error_description in the exception message and expose http_status, error, and error_description readers. Keep InvalidGrantError classification and refresh recovery behavior unchanged. Malformed responses retain the status-only fallback.

Only the two diagnostic fields are included; other response fields and the raw body are excluded. Normalize text to the RFC's printable ASCII set and cap error codes at 128 characters and descriptions at 512. Document that provider-controlled descriptions may still contain sensitive information and require application logging policies.

Validation:

  • Full test suite: 1,789 tests, 4,764 assertions, no failures or errors (Ruby 3.4.5).
  • Regression coverage for authorization-code and refresh failures, invalid grants, malformed bodies, unexpected field types, sanitization, truncation, and exclusion of unrelated response fields.
  • git diff --check passed.
  • RuboCop reports 30 existing offenses; no new offenses introduced.

nil
# RFC 6749 permits printable ASCII except double quotes and backslashes.
# Replace other characters to keep provider text on one log line.
value = value.gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ").strip

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.parse keeps invalid UTF-8 inside a JSON string (the result is a UTF-8 String with valid_encoding? false), and this gsub then raises ArgumentError: invalid byte sequence in UTF-8 while the exception is still being built. The rescue JSON::ParserError above does not cover it, nor do the InvalidGrantError / AuthorizationError rescues in MCP::Client::HTTP's refresh path, so one invalid byte in error_description turns a refresh failure into an ArgumentError out of the client call and skips the refresh-recovery decision. main never touches the description, so this is new here; it reproduces on Ruby 3.4.5 / json 2.19.7 and Ruby 4.0.6 / json 3.0.2:

value = JSON.parse("{\"error\":\"invalid_grant\",\"error_description\":\"bad \xFF byte\"}".b)["error_description"]
value.gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ") # => ArgumentError: invalid byte sequence in UTF-8

Scrub first, as the server-side Challenge#quote does for the same reason:

Suggested change
value = value.gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ").strip
value = value.scrub(" ").gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ").strip

Also wrap the diagnostic extraction in token_endpoint_error with a broader rescue that falls back to the status-only message, so building the exception can never raise, and add tests: error exactly "invalid_grant" with an invalid byte in error_description still raises InvalidGrantError with the sanitized description, and invalid bytes in both fields still yield an AuthorizationError.

)
```

### Token endpoint errors

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Token endpoint errors
### Token Endpoint Errors

Comment on lines +1107 to +1108
error = token_endpoint_diagnostic(parsed["error"], limit: 128)
description = token_endpoint_diagnostic(parsed["error_description"], limit: 512)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These limits define behavior the docs state; please name them next to the class's other constants so the code and the docs point at one place.

Suggested change
error = token_endpoint_diagnostic(parsed["error"], limit: 128)
description = token_endpoint_diagnostic(parsed["error_description"], limit: 512)
error = token_endpoint_diagnostic(parsed["error"], limit: TOKEN_ENDPOINT_ERROR_MAX_LENGTH)
description = token_endpoint_diagnostic(parsed["error_description"], limit: TOKEN_ENDPOINT_ERROR_DESCRIPTION_MAX_LENGTH)

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.

2 participants