diff --git a/lib/mcp_client/auth.rb b/lib/mcp_client/auth.rb index 6ffadfc..37e3348 100644 --- a/lib/mcp_client/auth.rb +++ b/lib/mcp_client/auth.rb @@ -288,10 +288,38 @@ class PKCE attr_reader :code_verifier, :code_challenge, :code_challenge_method # Generate PKCE parameters - def initialize - @code_verifier = generate_code_verifier - @code_challenge = generate_code_challenge(@code_verifier) - @code_challenge_method = 'S256' + # @param code_verifier [String, nil] Existing code verifier (for deserialization) + # @param code_challenge [String, nil] Existing code challenge (for deserialization) + # @param code_challenge_method [String] Challenge method (default: 'S256') + def initialize(code_verifier: nil, code_challenge: nil, code_challenge_method: nil) + @code_verifier = code_verifier || generate_code_verifier + @code_challenge = code_challenge || generate_code_challenge(@code_verifier) + @code_challenge_method = code_challenge_method || 'S256' + end + + # Convert to hash for serialization + # @return [Hash] Hash representation + def to_h + { + code_verifier: @code_verifier, + code_challenge: @code_challenge, + code_challenge_method: @code_challenge_method + } + end + + # Create PKCE instance from hash + # @param data [Hash] Hash with PKCE parameters + # @return [PKCE] New PKCE instance + # @raise [ArgumentError] If required parameters are missing + def self.from_h(data) + verifier = data[:code_verifier] || data['code_verifier'] + challenge = data[:code_challenge] || data['code_challenge'] + method = data[:code_challenge_method] || data['code_challenge_method'] + + raise ArgumentError, 'Missing code_verifier' unless verifier + raise ArgumentError, 'Missing code_challenge' unless challenge + + new(code_verifier: verifier, code_challenge: challenge, code_challenge_method: method) end private diff --git a/spec/lib/mcp_client/auth_spec.rb b/spec/lib/mcp_client/auth_spec.rb index eba6b36..e0bc3f2 100644 --- a/spec/lib/mcp_client/auth_spec.rb +++ b/spec/lib/mcp_client/auth_spec.rb @@ -124,6 +124,31 @@ expect(pkce1.code_verifier).not_to eq(pkce2.code_verifier) expect(pkce1.code_challenge).not_to eq(pkce2.code_challenge) end + + it 'accepts existing values' do + pkce = described_class.new( + code_verifier: 'test_verifier', + code_challenge: 'test_challenge', + code_challenge_method: 'S256' + ) + expect(pkce.code_verifier).to eq('test_verifier') + expect(pkce.code_challenge).to eq('test_challenge') + expect(pkce.code_challenge_method).to eq('S256') + end + end + + describe '#to_h and .from_h' do + it 'round-trips data with symbol and string keys' do + original = described_class.new + restored = described_class.from_h(original.to_h) + expect(restored.code_verifier).to eq(original.code_verifier) + expect(restored.code_challenge).to eq(original.code_challenge) + expect(restored.code_challenge_method).to eq('S256') + end + + it 'raises error when required fields are missing' do + expect { described_class.from_h({}) }.to raise_error(ArgumentError) + end end end end