Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/mcp/client/oauth/flow.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require "base64"
require "json"
require "openssl"
require "securerandom"
Expand Down Expand Up @@ -1653,7 +1652,7 @@ def reported_url(url)
def basic_auth_credentials(client_id, client_secret)
encoded_id = URI.encode_www_form_component(client_id)
encoded_secret = URI.encode_www_form_component(client_secret)
Base64.strict_encode64("#{encoded_id}:#{encoded_secret}")
["#{encoded_id}:#{encoded_secret}"].pack("m0")
end

def http_get(url)
Expand Down
5 changes: 3 additions & 2 deletions lib/mcp/client/oauth/jwt_client_assertion.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require "base64"
require "json"
require "openssl"
require "securerandom"
Expand Down Expand Up @@ -118,8 +117,10 @@ def ecdsa_der_to_raw(der)
end.join.then { |hex| [hex].pack("H*") }
end

# base64url without padding (RFC 7515 Section 2), spelled with `pack` because `base64` is no longer
# a default gem as of Ruby 3.4 and this gem does not depend on it.
def base64url(data)
Base64.urlsafe_encode64(data, padding: false)
[data].pack("m0").tr("+/", "-_").delete("=")
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/mcp/client/oauth/pkce.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require "base64"
require "digest"
require "securerandom"

Expand All @@ -27,7 +26,9 @@ class << self
# Generates a PKCE pair (code_verifier and S256 code_challenge).
def generate
verifier = SecureRandom.urlsafe_base64(64)
challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
# base64url without padding (RFC 7636 Appendix A), spelled with `pack` because `base64` is no longer
# a default gem as of Ruby 3.4 and this gem does not depend on it.
challenge = [Digest::SHA256.digest(verifier)].pack("m0").tr("+/", "-_").delete("=")

{ code_verifier: verifier, code_challenge: challenge, code_challenge_method: "S256" }
end
Expand Down
42 changes: 42 additions & 0 deletions test/mcp/client/oauth/base64_dependency_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require "test_helper"
require "open3"

module MCP
class Client
module OAuth
class Base64DependencyTest < ActiveSupport::TestCase
# `base64` stopped being a default gem in Ruby 3.4 and the gemspec does not declare it, so under Bundler
# an application that does not happen to get it from another gem cannot load it. Checked in a fresh process,
# since this one has loaded it for the tests already, with the library refused the way Bundler refuses it.
test "the client and the server-side OAuth classes load and encode without the base64 library" do
lib = File.expand_path("../../../../lib", __dir__)
script = <<~'RUBY'
module RefuseBase64
def require(name)
raise LoadError, "cannot load such file -- base64" if name == "base64"

super
end
end
Object.prepend(RefuseBase64)

require "mcp"
MCP::Client
pair = MCP::Client::OAuth::PKCE.generate
abort("unexpected code_challenge: #{pair[:code_challenge]}") unless pair[:code_challenge].match?(/\A[A-Za-z0-9_-]{43}\z/)
MCP::Client::OAuth::JWTClientAssertion
MCP::Server::OAuth::ProtectedResourceMetadata.new(
resource: "http://localhost:3000/mcp", authorization_servers: ["http://localhost:3000"],
)
abort("base64 was loaded") if defined?(Base64)
RUBY
_stdout, stderr, status = Open3.capture3({ "RUBYOPT" => nil }, RbConfig.ruby, "-I", lib, "-e", script)

assert_predicate status, :success?, stderr
end
end
end
end
end
Loading