Skip to content

Conversation

@jhrozek
Copy link
Contributor

@jhrozek jhrozek commented Oct 3, 2025

Add a token exchange client library that enables ToolHive to exchange incoming access tokens for backend-specific tokens when proxying MCP server requests. The implementation follows RFC 8693 and is compatible with standard oauth2.TokenSource interfaces.

Fixes #2064

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 3, 2025

@claude review this PR. Make sure to review it from the security perspective and read RFC 8693 before the review.

@claude
Copy link
Contributor

claude bot commented Oct 3, 2025

Claude finished @jhrozek's task —— View job


OAuth 2.0 Token Exchange (RFC 8693) Security Review

I've completed a comprehensive security review of this Token Exchange implementation. Here are my findings:

Review Tasks:

  • Read RFC 8693 specification to understand Token Exchange standard (Using existing knowledge)
  • Review pkg/auth/tokenexchange/exchange.go implementation
  • Review pkg/auth/tokenexchange/exchange_test.go test coverage
  • Analyze security aspects (input validation, token handling, error disclosure)
  • Check RFC 8693 compliance
  • Verify proper integration with oauth2.TokenSource interfaces
  • Provide comprehensive security review feedback

🔒 Security Analysis Summary

Overall Assessment: SECURE
This implementation demonstrates excellent security practices and RFC 8693 compliance. No critical security vulnerabilities identified.

🟢 Security Strengths

1. Credential Protection Excellence

  • Token Redaction: All sensitive data properly redacted in string representations (String() methods)
    • exchange.go:101-116: Subject tokens shown as [REDACTED]
    • exchange.go:129-142: Access/refresh tokens redacted in responses
    • exchange.go:151-159: Client secrets properly redacted
  • Test Coverage: Comprehensive credential leakage tests (exchange_test.go:555-616)

2. Robust Input Validation

  • Required Field Validation:
    • exchange.go:189-209: Config validation with proper URL parsing
    • exchange.go:327-329: Subject token requirement enforced
  • URL Validation: Uses url.Parse() for endpoint validation (line 203-206)

3. DoS Protection Measures

  • Response Size Limiting: maxResponseBodySize = 1MB (line 34)
    • Properly implemented with io.LimitReader (exchange.go:403)
    • Test coverage in exchange_test.go:524-553
  • HTTP Timeouts: 30-second default timeout configured (line 31)

4. Error Handling Security

  • Information Disclosure Prevention:
    • Generic error messages that don't leak sensitive implementation details
    • OAuth errors properly parsed and formatted (exchange.go:43-69)
    • Debug logging only for non-sensitive information
  • No Credential Leakage in Errors: Tested extensively (exchange_test.go:555-616)

🟡 Areas for Security Consideration

1. SSRF Protection Awareness

  • Current State: No explicit SSRF prevention in the library itself
  • Assessment: This is acceptable for a library design - SSRF protection should be at the application layer
  • Recommendation: Document that applications should validate TokenURL against allowlists
  • Test Coverage: Good test showing library doesn't prevent/facilitate SSRF (exchange_test.go:1025-1072)

2. Transport Security

  • Current: Relies on standard http.Client for TLS configuration
  • Recommendation: Document TLS requirements for production use
  • Note: Applications should configure HTTPClient with proper TLS settings

🟢 RFC 8693 Compliance Analysis

Required Parameters ✅

  • grant_type: Correctly set to urn:ietf:params:oauth:grant-type:token-exchange
  • subject_token: Required and validated
  • subject_token_type: Defaults to access_token as per RFC

Optional Parameters ✅

  • audience: Properly supported
  • scope: Array correctly converted to space-separated string
  • resource: Supported
  • actor_token/actor_token_type: Delegation scenarios supported

Response Handling ✅

  • All RFC 8693 response fields properly parsed
  • expires_in correctly converted to oauth2.Token.Expiry
  • Minimal response support (only access_token + token_type)

🟢 Integration Assessment

oauth2.TokenSource Compatibility ✅

  • Properly implements Token() (*oauth2.Token, error) interface
  • Correct token expiry handling
  • Refresh token support

Design Patterns ✅

  • Clean separation of concerns
  • Testable design with dependency injection (SubjectTokenProvider function)
  • Proper context propagation for cancellation

📋 Minor Enhancement Suggestions

  1. Documentation Enhancement: Add godoc examples for common usage patterns
  2. TLS Guidance: Document recommended TLS configurations for production
  3. SSRF Guidance: Add security documentation about URL validation requirements

🧪 Test Coverage Analysis

Excellent test coverage with 26 comprehensive test scenarios covering:

  • ✅ Happy path scenarios
  • ✅ Error conditions and edge cases
  • ✅ RFC compliance verification
  • ✅ Security boundary testing (credential leakage, DoS protection)
  • ✅ Network failure handling
  • ✅ Malformed response handling
  • ✅ Context cancellation

🚀 Conclusion

This is a high-quality, security-conscious implementation of RFC 8693. The code demonstrates:

  • Strong security practices with comprehensive credential protection
  • Excellent RFC compliance
  • Robust error handling and input validation
  • Comprehensive test coverage including security scenarios
  • Clean, maintainable design

Recommendation: APPROVE - This implementation meets enterprise security standards and is ready for production use.


Job Run: Link

@JAORMX JAORMX requested a review from Copilot October 3, 2025 13:29
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements OAuth 2.0 Token Exchange (RFC 8693) support for ToolHive by adding a token exchange client library. The implementation allows exchanging incoming access tokens for backend-specific tokens when proxying MCP server requests, following RFC 8693 standards and being compatible with standard oauth2.TokenSource interfaces.

  • Adds a comprehensive token exchange client library with Config, TokenSource implementation, and HTTP request handling
  • Implements proper error handling for OAuth errors, network failures, and malformed responses
  • Includes extensive test coverage for happy path, error scenarios, edge cases, and security considerations

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
pkg/auth/tokenexchange/exchange.go Core implementation of OAuth 2.0 token exchange with Config, TokenSource, and HTTP handling
pkg/auth/tokenexchange/exchange_test.go Comprehensive test suite covering success cases, error handling, and edge cases

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@codecov
Copy link

codecov bot commented Oct 3, 2025

Codecov Report

❌ Patch coverage is 72.41379% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.81%. Comparing base (bc1d063) to head (2c79102).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
pkg/auth/tokenexchange/exchange.go 72.41% 53 Missing and 11 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2082      +/-   ##
==========================================
+ Coverage   48.38%   48.81%   +0.43%     
==========================================
  Files         238      241       +3     
  Lines       30034    30496     +462     
==========================================
+ Hits        14531    14888     +357     
- Misses      14401    14492      +91     
- Partials     1102     1116      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 6, 2025

@claude review only the 2nd patch in this PR, the first one is unchanged

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@claude
Copy link
Contributor

claude bot commented Oct 6, 2025

Claude finished @jhrozek's task —— View job


Review of 2nd Patch: Basic Auth Implementation

Task List:

  • Identify the 2 commits in this PR
  • Extract changes from the 2nd commit only
  • Read and analyze the 2nd commit's changes
  • Review against RFC 8693 and RFC 6749 compliance
  • Check if previous review comments were addressed
  • Provide security-focused feedback on the new changes

🔒 Security Review: HTTP Basic Auth for Client Credentials

Reviewing commit 1f90f02 - "Use HTTP Basic Auth for OAuth 2.0 Token Exchange client credentials"

📋 Summary of Changes

This patch migrates client authentication from POST body parameters (client_id/client_secret) to HTTP Basic Authentication via the Authorization header, following RFC 6749 Section 2.3.1 recommendations.

Key Changes:

  • Removed addClientAuthentication() function that added credentials to form body
  • Modified createTokenExchangeRequest() to accept auth parameter and set Basic Auth header
  • Added URL encoding of credentials before passing to SetBasicAuth() (line 390)
  • Updated all tests to verify credentials are in Authorization header, not in request body
  • Added new test for URL encoding of special characters in credentials

✅ Security Improvements

1. Credential Exposure Reduction

  • Excellent improvement: Moving credentials from POST body to Authorization header
  • Rationale: HTTP stacks typically provide special handling for Authorization headers
    • Less likely to be logged by proxies, load balancers, and monitoring systems
    • Many logging frameworks automatically redact Authorization headers
    • POST body parameters are commonly logged in full

2. Support for Public Clients

The implementation correctly handles public clients (line 389):

if auth.ClientID != "" && auth.ClientSecret != "" {
    req.SetBasicAuth(url.QueryEscape(auth.ClientID), url.QueryEscape(auth.ClientSecret))
}
  • Only sets Basic Auth when both ClientID and ClientSecret are present
  • Properly supports public clients (no secret) by omitting the Authorization header
  • However, see note below about validation inconsistency

3. Comprehensive Test Coverage

New tests added:

  • Verification that credentials are in Authorization header (not body)
  • Public client scenarios (empty credentials, only ClientID)
  • Special character handling with URL encoding

🔴 Critical Issue: Incorrect URL Encoding Implementation

Location: pkg/auth/tokenexchange/exchange.go:390

Problem: The code URL-encodes credentials before passing to SetBasicAuth():

req.SetBasicAuth(url.QueryEscape(auth.ClientID), url.QueryEscape(auth.ClientSecret))

RFC 6749 Section 2.3.1 Requirements:

The client identifier is encoded using the "application/x-www-form-urlencoded" encoding algorithm per Appendix B, and the encoded value is used as the username; the client password is encoded using the same algorithm and used as the password.

The Issue:

  • RFC 6749 requires credentials to be percent-encoded (URL-encoded) BEFORE being base64-encoded for Basic Auth
  • However, Go's http.Request.SetBasicAuth() does NOT perform any encoding - it only base64-encodes the username:password string
  • This means the current implementation is correctly URL-encoding the credentials first, then letting SetBasicAuth base64-encode them ✅

Wait, Actually...

Looking at the test expectations in exchange_test.go:1274-1275:

assert.Equal(t, url.QueryEscape(specialClientID), username, "ClientID should be URL-encoded")
assert.Equal(t, url.QueryEscape(specialClientSecret), password, "ClientSecret should be URL-encoded")

This test verifies that after r.BasicAuth() decodes the base64, the credentials are still URL-encoded. This is CORRECT per RFC 6749 Section 2.3.1!

✅ Implementation is RFC 6749 Compliant

After careful analysis, the implementation is correct:

  1. Credentials are URL-encoded first: url.QueryEscape(auth.ClientID)
  2. Then base64-encoded by SetBasicAuth(): encodes "encoded_id:encoded_secret"
  3. On the server side, base64 decode reveals the URL-encoded credentials
  4. Server must URL-decode them to get the original values

This matches RFC 6749 Section 2.3.1 exactly. ✅


🟡 Validation Inconsistency (Minor)

Location: pkg/auth/tokenexchange/exchange.go:198-200

The Config.Validate() method still requires ClientID:

if c.ClientID == "" {
    return fmt.Errorf("ClientID is required")
}

However, the implementation at line 389 supports public clients (empty ClientID/ClientSecret).

Assessment:

  • This appears to be an intentional design decision for ToolHive's use case
  • The previous review comment suggested making ClientID optional
  • The PR author (jhrozek) responded that the IDPs they work with only support confidential clients
  • Recommendation: This is acceptable if ToolHive's requirements mandate confidential clients only

Suggestion: Add a comment in the validation explaining this design choice:

// ClientID is required for confidential clients. Public clients are not supported
// in this implementation as the target IDPs require client authentication.
if c.ClientID == "" {
    return fmt.Errorf("ClientID is required")
}

🟢 Test Coverage Analysis

Excellent test additions:

  1. Basic Auth Verification (exchange_test.go:36-44):

    • Verifies Authorization header is present
    • Verifies Basic scheme is used
    • Validates credentials via r.BasicAuth()
  2. Body Parameter Removal (exchange_test.go:59-61):

    • Explicitly checks that client_id and client_secret are NOT in request body
    • Critical for security - prevents credential exposure in logs
  3. Public Client Support (exchange_test.go:808-813, 848-853):

    • Empty credentials: No Authorization header
    • Only ClientID: No Authorization header (requires both for Basic Auth)
  4. Special Character Encoding (exchange_test.go:1256-1300):

    • Tests credentials with :, @, /, &, =, % characters
    • Verifies URL encoding is applied correctly
    • Validates server can decode properly

📊 RFC 6749 Compliance Summary

Requirement Status Notes
Use HTTP Basic Auth for confidential clients Implemented correctly
Encode credentials using application/x-www-form-urlencoded Using url.QueryEscape()
Base64-encode the result SetBasicAuth() handles this
Support public clients (no credentials) Omits Authorization header when credentials empty
Remove credentials from POST body Credentials only in Authorization header

🚀 Security Assessment: APPROVED ✅

Overall Rating: Secure and RFC-Compliant

Strengths:

  • Correct implementation of RFC 6749 Section 2.3.1
  • Improved security posture by moving credentials to Authorization header
  • Comprehensive test coverage including edge cases
  • Proper handling of special characters in credentials
  • Clean migration from body parameters to Basic Auth

Minor Suggestions:

  1. Add documentation comment explaining why ClientID is required (ToolHive-specific requirement)
  2. Consider documenting that public clients are intentionally not supported

No security vulnerabilities identified in this patch.


📝 Code Quality Notes

  • Clean refactoring with clear separation of concerns
  • Good commit message explaining the "why" behind the change
  • Test coverage is thorough and well-structured
  • Code follows Go idioms and best practices

Recommendation: APPROVE - This patch successfully implements RFC 6749-compliant Basic Authentication for client credentials with improved security characteristics.


Branch: thv-te-iface
Job Run: View run

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 6, 2025

@claude one more review of 53f5360 please

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@claude
Copy link
Contributor

claude bot commented Oct 6, 2025

Claude finished @jhrozek's task —— View job


Review of 2nd Commit: HTTP Basic Auth Implementation

Task List:

  • Identify and extract the second commit changes
  • Read the modified files in the second commit
  • Analyze Basic Auth implementation against RFC 6749 Section 2.3.1
  • Review security aspects of credential handling
  • Evaluate Copilot's review comments about URL encoding
  • Provide comprehensive security review feedback

🔍 Review Summary

Commit: 53f5360 - "Use HTTP Basic Auth for OAuth 2.0 Token Exchange client credentials"

Overall Assessment: The migration to HTTP Basic Auth is a security improvement, but there is a critical RFC compliance issue that must be fixed.


🔴 Critical Issue: Missing URL Encoding for Client Credentials

Location: pkg/auth/tokenexchange/exchange.go:390

The Problem

The current implementation:

req.SetBasicAuth(auth.ClientID, auth.ClientSecret)

RFC 6749 Section 2.3.1 Requirements:

The client identifier is encoded using the "application/x-www-form-urlencoded" encoding algorithm per Appendix B, and the encoded value is used as the username; the client password is encoded using the same algorithm and used as the password.

Go Standard Library Documentation (go doc net/http.Request.SetBasicAuth):

When used with OAuth2, both arguments must be URL encoded first with url.QueryEscape.

Why This Matters

  1. RFC Compliance: RFC 6749 explicitly requires URL encoding before base64 encoding
  2. Special Character Handling: Credentials containing :, @, %, &, =, etc. will fail without proper encoding
  3. Interoperability: OAuth servers expect properly encoded credentials per the spec

The Fix

Required change:

req.SetBasicAuth(url.QueryEscape(auth.ClientID), url.QueryEscape(auth.ClientSecret))

Missing Test Coverage

The commit does not include a test for special characters in client credentials. You must add a test like:

func TestExchangeToken_SpecialCharactersInCredentials(t *testing.T) {
    specialClientID := "client:with@special/chars"
    specialClientSecret := "secret&with%chars="
    
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        username, password, ok := r.BasicAuth()
        require.True(t, ok)
        
        // Verify credentials are URL-encoded
        assert.Equal(t, url.QueryEscape(specialClientID), username)
        assert.Equal(t, url.QueryEscape(specialClientSecret), password)
        
        // Return success
        // ...
    }))
    // ...
}

✅ Security Improvements (Positive Changes)

1. Credential Exposure Reduction ⭐

Moving credentials from POST body to Authorization header is an excellent security improvement:

  • Less Logging Exposure: Most HTTP stacks automatically redact Authorization headers but log POST body parameters in full
  • Proxy Safety: Proxies and load balancers typically handle Authorization headers with special care
  • Monitoring Systems: Security monitoring tools often filter Authorization headers by default

2. Clean Migration

The implementation correctly:

  • Removes addClientAuthentication() function that added credentials to form body
  • Updates all tests to verify credentials are in Authorization header
  • Explicitly checks that credentials are NOT in request body (lines 60-61 of test file)

3. Public Client Support

The implementation properly handles public clients (line 389):

if auth.ClientID != "" && auth.ClientSecret != "" {
    req.SetBasicAuth(auth.ClientID, auth.ClientSecret)

Only sets Basic Auth when both ClientID and ClientSecret are present, which is correct behavior.


🟡 Minor Observations

1. Validation Consistency

The Config.Validate() method still requires ClientID (line 198-200 in exchange.go), but the implementation supports public clients. Based on previous review comments, this appears to be an intentional design decision for ToolHive's use case (confidential clients only).

Recommendation: Add a documentation comment explaining this:

// ClientID is required. This implementation only supports confidential clients
// as required by ToolHive's target identity providers.
if c.ClientID == "" {
    return fmt.Errorf("ClientID is required")
}

2. Comment Accuracy

The comment at line 388 states:

SetBasicAuth handles UTF-8 encoding and base64 encoding internally

This is partially incorrect. SetBasicAuth only handles base64 encoding. It does NOT handle URL encoding, which is why we need to add it explicitly per RFC 6749 and Go's documentation.

Recommended comment:

// Add client authentication via HTTP Basic Auth per RFC 6749 Section 2.3.1
// Per RFC 6749, credentials must be URL-encoded before base64 encoding.
// SetBasicAuth handles the base64 encoding step.
if auth.ClientID != "" && auth.ClientSecret != "" {
    req.SetBasicAuth(url.QueryEscape(auth.ClientID), url.QueryEscape(auth.ClientSecret))
}

📋 Test Coverage Analysis

Good test additions:

  1. ✅ Verification that credentials are in Authorization header (lines 36-44)
  2. ✅ Verification that credentials are NOT in POST body (lines 59-61)
  3. ✅ Public client scenarios (empty credentials, only ClientID)

Missing critical test:

  1. Special characters in client credentials - This is essential for RFC compliance verification

🚀 Action Items

Required (Blocker)

  1. Add URL encoding to line 390:

    req.SetBasicAuth(url.QueryEscape(auth.ClientID), url.QueryEscape(auth.ClientSecret))
  2. Add test for special characters in client credentials to verify RFC compliance

  3. Update comment at line 387-388 to accurately describe the encoding requirements

Optional (Enhancement)

  1. Add documentation comment to Config.Validate() explaining why ClientID is required

📊 Copilot Review Comments Assessment

Copilot's review comments were correct:

  1. ✅ Correctly identified missing URL encoding in exchange.go:390
  2. ✅ Correctly suggested that credentials should NOT be URL-encoded in the test expectations (after basic auth decoding)
  3. ❌ The suggestion about invalid.test hostname is minor and optional

Note: There appears to be confusion in Copilot's latest review. The test expectations ARE correct - after BasicAuth() decodes the base64, the credentials should still be URL-encoded (which the server then URL-decodes). However, the implementation is missing the URL encoding step.


🎯 Final Assessment

Status: CHANGES REQUIRED ⚠️

Rationale:

  • The migration to HTTP Basic Auth is a good security improvement ✅
  • However, the implementation violates RFC 6749 Section 2.3.1 by not URL-encoding credentials ❌
  • This will cause failures with credentials containing special characters ❌
  • Missing test coverage for special characters ❌

Recommendation: REQUEST CHANGES - Fix URL encoding issue and add test coverage before merging.


Branch: thv-te-iface
Commit: 53f5360
Job Run: View run

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 6, 2025

The AI reviews are funny when 2 LLMs disagree. But looking at the RFC, Claude was right and Copilot wrong. @claude one more review please.

@claude
Copy link
Contributor

claude bot commented Oct 6, 2025

Claude finished @jhrozek's task —— View job


Final Review of OAuth 2.0 Token Exchange Implementation

Task List:

  • Check the current state of the PR changes
  • Read exchange.go implementation
  • Read exchange_test.go test coverage
  • Verify RFC 6749 Section 2.3.1 compliance
  • Review URL encoding implementation
  • Provide final security assessment

✅ Final Security Assessment: APPROVED

You were absolutely correct - Claude's previous review was right and Copilot's suggestions were incorrect. The implementation now properly follows RFC 6749 Section 2.3.1.


🟢 RFC 6749 Section 2.3.1 Compliance: CONFIRMED

Location: pkg/auth/tokenexchange/exchange.go:391

The implementation correctly URL-encodes client credentials before passing them to SetBasicAuth():

req.SetBasicAuth(url.QueryEscape(auth.ClientID), url.QueryEscape(auth.ClientSecret))

Why This is Correct:

  1. RFC 6749 Section 2.3.1 states:

    The client identifier is encoded using the "application/x-www-form-urlencoded" encoding algorithm per Appendix B, and the encoded value is used as the username; the client password is encoded using the same algorithm and used as the password.

  2. Go's SetBasicAuth() documentation explicitly states:

    When used with OAuth2, both arguments must be URL encoded first with url.QueryEscape.

  3. The encoding sequence is:

    • Step 1: URL-encode credentials (done manually with url.QueryEscape)
    • Step 2: Concatenate as username:password
    • Step 3: Base64-encode the result (done by SetBasicAuth())
  4. On the server side:

    • Step 1: Base64-decode the Authorization header
    • Step 2: Split on : to get username and password
    • Step 3: URL-decode each to get original credentials

🟢 Excellent Test Coverage

Location: pkg/auth/tokenexchange/exchange_test.go:1259-1303

The TestExchangeToken_BasicAuthURLEncoding test comprehensively validates the URL encoding implementation:

specialClientID := "client:with@special/chars"
specialClientSecret := "secret&with=special%chars"

Test validates:

  • ✅ Authorization header is present with Basic scheme
  • ✅ Credentials are URL-encoded after BasicAuth() decodes the base64
  • ✅ Special characters (:, @, /, &, =, %) are handled correctly
  • ✅ Server can properly parse the credentials

Why the test expectations are correct (lines 1277-1278):

assert.Equal(t, url.QueryEscape(specialClientID), username, "ClientID should be URL-encoded")
assert.Equal(t, url.QueryEscape(specialClientSecret), password, "ClientSecret should be URL-encoded")

After r.BasicAuth() decodes the base64, it extracts the raw username/password strings. These strings should still be URL-encoded because that's what RFC 6749 requires - the server must URL-decode them to get the original credentials.


🎯 Why Copilot Was Wrong

Copilot's incorrect suggestion:

Client credentials should not be URL-encoded before being passed to SetBasicAuth

Why this is wrong:

  1. Contradicts RFC 6749 Section 2.3.1 explicit requirement
  2. Contradicts Go standard library documentation for OAuth2 usage
  3. Would cause authentication failures with special characters in credentials
  4. The RFC specifically mandates URL encoding before base64 encoding

The confusion: SetBasicAuth() only base64-encodes - it does NOT URL-encode. For general HTTP Basic Auth, URL encoding isn't needed. But OAuth 2.0 has stricter requirements per RFC 6749.


🔒 Security Strengths (from previous reviews still apply)

  1. Credential Protection Excellence

    • All sensitive tokens properly redacted in String() methods
    • Comprehensive credential leakage tests
  2. DoS Protection

    • Response size limited to 1MB
    • HTTP timeouts configured
  3. Error Handling

    • No sensitive information disclosure in errors
    • Proper OAuth error parsing
  4. Clean Migration to Basic Auth

    • Credentials removed from POST body (lines 1254-1255 test verification)
    • Authorization header properly configured
    • Public client support (empty credentials)

📊 Complete Test Suite

The PR includes 30 comprehensive test scenarios covering:

  • ✅ Happy path with various token types
  • ✅ Error conditions (network, HTTP, malformed responses)
  • ✅ RFC 6749/8693 compliance verification
  • ✅ Security boundaries (credential leakage, DoS protection)
  • Special character handling in credentials (RFC 6749 compliance)
  • ✅ Context cancellation
  • ✅ Edge cases (empty fields, optional parameters, minimal responses)

🚀 Final Verdict

Status: ✅ APPROVED - RFC COMPLIANT

Key Findings:

  1. ✅ URL encoding implementation is correct per RFC 6749 Section 2.3.1
  2. ✅ Test coverage is comprehensive and validates RFC compliance
  3. ✅ Security practices are excellent throughout
  4. ✅ Migration to HTTP Basic Auth is a security improvement
  5. ✅ The code follows Go best practices and OAuth 2.0 standards

Recommendation: This implementation is production-ready and should be merged. It correctly implements OAuth 2.0 Token Exchange (RFC 8693) with proper client authentication per RFC 6749 Section 2.3.1.


Additional Notes:

  • The comment at line 388-389 is helpful and accurate
  • The validation at line 198-200 (requiring ClientID) is a design decision for confidential clients only
  • All review comments from Copilot about URL encoding should be disregarded as they contradict the RFC

Branch: thv-te-iface
Job Run: View run

JAORMX
JAORMX previously approved these changes Oct 7, 2025
Add a token exchange client library that enables ToolHive to exchange
incoming access tokens for backend-specific tokens when proxying MCP
server requests. The implementation follows RFC 8693 and is compatible
with standard oauth2.TokenSource interfaces.

Fixes #2064
Change token exchange client authentication from body parameters
(client_id/client_secret in POST body) to HTTP Basic Authentication
via Authorization header, following RFC 6749 Section 2.3.1 recommendations.

This reduces credential exposure risk in HTTP logs and monitoring systems,
as the Authorization header receives special handling throughout the HTTP
stack while body parameters are commonly logged.
yrobla
yrobla previously approved these changes Oct 7, 2025
@jhrozek jhrozek dismissed stale reviews from yrobla and JAORMX via 429f55d October 7, 2025 13:39
JAORMX
JAORMX previously approved these changes Oct 7, 2025
@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 7, 2025

I'm sorry one more push, I added the extra checks but some test cases were returning mocked tokens which were not passing the checks

@jhrozek jhrozek merged commit 3b0ff07 into main Oct 7, 2025
26 checks passed
@jhrozek jhrozek deleted the thv-te-iface branch October 7, 2025 17:13
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.

Add a basic implementation of token exchange

4 participants