ruby_llm_swarm-mcp 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
- data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
- data/lib/ruby_llm/chat.rb +34 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
- data/lib/ruby_llm/mcp/attachment.rb +18 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +401 -0
- data/lib/ruby_llm/mcp/completion.rb +16 -0
- data/lib/ruby_llm/mcp/configuration.rb +310 -0
- data/lib/ruby_llm/mcp/content.rb +28 -0
- data/lib/ruby_llm/mcp/elicitation.rb +48 -0
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +91 -0
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
- data/lib/ruby_llm/mcp/native/client.rb +387 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
- data/lib/ruby_llm/mcp/native/messages.rb +36 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
- data/lib/ruby_llm/mcp/progress.rb +35 -0
- data/lib/ruby_llm/mcp/prompt.rb +132 -0
- data/lib/ruby_llm/mcp/railtie.rb +14 -0
- data/lib/ruby_llm/mcp/resource.rb +112 -0
- data/lib/ruby_llm/mcp/resource_template.rb +85 -0
- data/lib/ruby_llm/mcp/result.rb +108 -0
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +152 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
- data/lib/ruby_llm/mcp/tool.rb +228 -0
- data/lib/ruby_llm/mcp/version.rb +7 -0
- data/lib/ruby_llm/mcp.rb +125 -0
- data/lib/tasks/release.rake +23 -0
- metadata +184 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "digest/sha2"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module RubyLLM
|
|
9
|
+
module MCP
|
|
10
|
+
module Auth
|
|
11
|
+
# OAuth configuration constants
|
|
12
|
+
# Token refresh buffer time in seconds (5 minutes)
|
|
13
|
+
TOKEN_REFRESH_BUFFER = 300
|
|
14
|
+
|
|
15
|
+
# Default OAuth timeout in seconds (30 seconds)
|
|
16
|
+
DEFAULT_OAUTH_TIMEOUT = 30
|
|
17
|
+
|
|
18
|
+
# CSRF state parameter size in bytes (32 bytes)
|
|
19
|
+
CSRF_STATE_SIZE = 32
|
|
20
|
+
|
|
21
|
+
# PKCE code verifier size in bytes (32 bytes)
|
|
22
|
+
PKCE_VERIFIER_SIZE = 32
|
|
23
|
+
|
|
24
|
+
# Represents an OAuth 2.1 access token with expiration tracking
|
|
25
|
+
class Token
|
|
26
|
+
attr_reader :access_token, :token_type, :expires_in, :scope, :refresh_token, :expires_at
|
|
27
|
+
|
|
28
|
+
def initialize(access_token:, token_type: "Bearer", expires_in: nil, scope: nil, refresh_token: nil)
|
|
29
|
+
@access_token = access_token
|
|
30
|
+
@token_type = token_type
|
|
31
|
+
@expires_in = expires_in
|
|
32
|
+
@scope = scope
|
|
33
|
+
@refresh_token = refresh_token
|
|
34
|
+
@expires_at = expires_in ? Time.now + expires_in : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if token has expired
|
|
38
|
+
# @return [Boolean] true if token is expired
|
|
39
|
+
def expired?
|
|
40
|
+
return false unless @expires_at
|
|
41
|
+
|
|
42
|
+
Time.now >= @expires_at
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if token expires soon (within configured buffer)
|
|
46
|
+
# This enables proactive token refresh
|
|
47
|
+
# @return [Boolean] true if token expires within the buffer period
|
|
48
|
+
def expires_soon?
|
|
49
|
+
return false unless @expires_at
|
|
50
|
+
|
|
51
|
+
Time.now >= (@expires_at - TOKEN_REFRESH_BUFFER)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Format token for Authorization header
|
|
55
|
+
# @return [String] formatted as "Bearer {access_token}"
|
|
56
|
+
def to_header
|
|
57
|
+
"#{@token_type} #{@access_token}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Serialize token to hash
|
|
61
|
+
# @return [Hash] token data
|
|
62
|
+
def to_h
|
|
63
|
+
{
|
|
64
|
+
access_token: @access_token,
|
|
65
|
+
token_type: @token_type,
|
|
66
|
+
expires_in: @expires_in,
|
|
67
|
+
scope: @scope,
|
|
68
|
+
refresh_token: @refresh_token,
|
|
69
|
+
expires_at: @expires_at&.iso8601
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Deserialize token from hash
|
|
74
|
+
# @param data [Hash] token data
|
|
75
|
+
# @return [Token] new token instance
|
|
76
|
+
def self.from_h(data)
|
|
77
|
+
token = new(
|
|
78
|
+
access_token: data[:access_token] || data["access_token"],
|
|
79
|
+
token_type: data[:token_type] || data["token_type"] || "Bearer",
|
|
80
|
+
expires_in: data[:expires_in] || data["expires_in"],
|
|
81
|
+
scope: data[:scope] || data["scope"],
|
|
82
|
+
refresh_token: data[:refresh_token] || data["refresh_token"]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Restore expires_at if present
|
|
86
|
+
expires_at_str = data[:expires_at] || data["expires_at"]
|
|
87
|
+
if expires_at_str
|
|
88
|
+
token.instance_variable_set(:@expires_at, Time.parse(expires_at_str))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
token
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Client metadata for dynamic client registration (RFC 7591)
|
|
96
|
+
# Supports all optional parameters from the specification
|
|
97
|
+
class ClientMetadata
|
|
98
|
+
attr_reader :redirect_uris, :token_endpoint_auth_method, :grant_types, :response_types, :scope,
|
|
99
|
+
:client_name, :client_uri, :logo_uri, :contacts, :tos_uri, :policy_uri,
|
|
100
|
+
:jwks_uri, :jwks, :software_id, :software_version
|
|
101
|
+
|
|
102
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
103
|
+
redirect_uris:,
|
|
104
|
+
token_endpoint_auth_method: "none",
|
|
105
|
+
grant_types: %w[authorization_code refresh_token],
|
|
106
|
+
response_types: ["code"],
|
|
107
|
+
scope: nil,
|
|
108
|
+
client_name: nil,
|
|
109
|
+
client_uri: nil,
|
|
110
|
+
logo_uri: nil,
|
|
111
|
+
contacts: nil,
|
|
112
|
+
tos_uri: nil,
|
|
113
|
+
policy_uri: nil,
|
|
114
|
+
jwks_uri: nil,
|
|
115
|
+
jwks: nil,
|
|
116
|
+
software_id: nil,
|
|
117
|
+
software_version: nil
|
|
118
|
+
)
|
|
119
|
+
@redirect_uris = redirect_uris
|
|
120
|
+
@token_endpoint_auth_method = token_endpoint_auth_method
|
|
121
|
+
@grant_types = grant_types
|
|
122
|
+
@response_types = response_types
|
|
123
|
+
@scope = scope
|
|
124
|
+
@client_name = client_name
|
|
125
|
+
@client_uri = client_uri
|
|
126
|
+
@logo_uri = logo_uri
|
|
127
|
+
@contacts = contacts
|
|
128
|
+
@tos_uri = tos_uri
|
|
129
|
+
@policy_uri = policy_uri
|
|
130
|
+
@jwks_uri = jwks_uri
|
|
131
|
+
@jwks = jwks
|
|
132
|
+
@software_id = software_id
|
|
133
|
+
@software_version = software_version
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Convert to hash for registration request
|
|
137
|
+
# @return [Hash] client metadata
|
|
138
|
+
def to_h
|
|
139
|
+
{
|
|
140
|
+
redirect_uris: @redirect_uris,
|
|
141
|
+
token_endpoint_auth_method: @token_endpoint_auth_method,
|
|
142
|
+
grant_types: @grant_types,
|
|
143
|
+
response_types: @response_types,
|
|
144
|
+
scope: @scope,
|
|
145
|
+
client_name: @client_name,
|
|
146
|
+
client_uri: @client_uri,
|
|
147
|
+
logo_uri: @logo_uri,
|
|
148
|
+
contacts: @contacts,
|
|
149
|
+
tos_uri: @tos_uri,
|
|
150
|
+
policy_uri: @policy_uri,
|
|
151
|
+
jwks_uri: @jwks_uri,
|
|
152
|
+
jwks: @jwks,
|
|
153
|
+
software_id: @software_id,
|
|
154
|
+
software_version: @software_version
|
|
155
|
+
}.compact
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Registered client information from authorization server
|
|
160
|
+
class ClientInfo
|
|
161
|
+
attr_reader :client_id, :client_secret, :client_id_issued_at, :client_secret_expires_at, :metadata
|
|
162
|
+
|
|
163
|
+
def initialize(client_id:, client_secret: nil, client_id_issued_at: nil, client_secret_expires_at: nil,
|
|
164
|
+
metadata: nil)
|
|
165
|
+
@client_id = client_id
|
|
166
|
+
@client_secret = client_secret
|
|
167
|
+
@client_id_issued_at = client_id_issued_at
|
|
168
|
+
@client_secret_expires_at = client_secret_expires_at
|
|
169
|
+
@metadata = metadata
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Check if client secret has expired
|
|
173
|
+
# @return [Boolean] true if client secret is expired
|
|
174
|
+
def client_secret_expired?
|
|
175
|
+
return false unless @client_secret_expires_at
|
|
176
|
+
|
|
177
|
+
Time.now.to_i >= @client_secret_expires_at
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Serialize to hash
|
|
181
|
+
# @return [Hash] client info
|
|
182
|
+
def to_h
|
|
183
|
+
{
|
|
184
|
+
client_id: @client_id,
|
|
185
|
+
client_secret: @client_secret,
|
|
186
|
+
client_id_issued_at: @client_id_issued_at,
|
|
187
|
+
client_secret_expires_at: @client_secret_expires_at,
|
|
188
|
+
metadata: @metadata&.to_h
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Deserialize from hash
|
|
193
|
+
# @param data [Hash] client info data
|
|
194
|
+
# @return [ClientInfo] new instance
|
|
195
|
+
def self.from_h(data)
|
|
196
|
+
metadata_data = data[:metadata] || data["metadata"]
|
|
197
|
+
metadata = if metadata_data
|
|
198
|
+
ClientMetadata.new(**metadata_data.transform_keys(&:to_sym))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
new(
|
|
202
|
+
client_id: data[:client_id] || data["client_id"],
|
|
203
|
+
client_secret: data[:client_secret] || data["client_secret"],
|
|
204
|
+
client_id_issued_at: data[:client_id_issued_at] || data["client_id_issued_at"],
|
|
205
|
+
client_secret_expires_at: data[:client_secret_expires_at] || data["client_secret_expires_at"],
|
|
206
|
+
metadata: metadata
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# OAuth Authorization Server Metadata (RFC 8414)
|
|
212
|
+
class ServerMetadata
|
|
213
|
+
attr_reader :issuer, :authorization_endpoint, :token_endpoint, :registration_endpoint,
|
|
214
|
+
:scopes_supported, :response_types_supported, :grant_types_supported
|
|
215
|
+
|
|
216
|
+
def initialize(issuer:, authorization_endpoint:, token_endpoint:, options: {})
|
|
217
|
+
@issuer = issuer
|
|
218
|
+
@authorization_endpoint = authorization_endpoint
|
|
219
|
+
@token_endpoint = token_endpoint
|
|
220
|
+
@registration_endpoint = options[:registration_endpoint] || options["registration_endpoint"]
|
|
221
|
+
@scopes_supported = options[:scopes_supported] || options["scopes_supported"]
|
|
222
|
+
@response_types_supported = options[:response_types_supported] || options["response_types_supported"]
|
|
223
|
+
@grant_types_supported = options[:grant_types_supported] || options["grant_types_supported"]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Check if dynamic client registration is supported
|
|
227
|
+
# @return [Boolean] true if registration endpoint exists
|
|
228
|
+
def supports_registration?
|
|
229
|
+
!@registration_endpoint.nil?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Serialize to hash
|
|
233
|
+
# @return [Hash] server metadata
|
|
234
|
+
def to_h
|
|
235
|
+
{
|
|
236
|
+
issuer: @issuer,
|
|
237
|
+
authorization_endpoint: @authorization_endpoint,
|
|
238
|
+
token_endpoint: @token_endpoint,
|
|
239
|
+
registration_endpoint: @registration_endpoint,
|
|
240
|
+
scopes_supported: @scopes_supported,
|
|
241
|
+
response_types_supported: @response_types_supported,
|
|
242
|
+
grant_types_supported: @grant_types_supported
|
|
243
|
+
}.compact
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Deserialize from hash
|
|
247
|
+
# @param data [Hash] server metadata
|
|
248
|
+
# @return [ServerMetadata] new instance
|
|
249
|
+
def self.from_h(data)
|
|
250
|
+
new(
|
|
251
|
+
issuer: data[:issuer] || data["issuer"],
|
|
252
|
+
authorization_endpoint: data[:authorization_endpoint] || data["authorization_endpoint"],
|
|
253
|
+
token_endpoint: data[:token_endpoint] || data["token_endpoint"],
|
|
254
|
+
registration_endpoint: data[:registration_endpoint] || data["registration_endpoint"],
|
|
255
|
+
scopes_supported: data[:scopes_supported] || data["scopes_supported"],
|
|
256
|
+
response_types_supported: data[:response_types_supported] || data["response_types_supported"],
|
|
257
|
+
grant_types_supported: data[:grant_types_supported] || data["grant_types_supported"]
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# OAuth Protected Resource Metadata (RFC 9728)
|
|
263
|
+
# Used for authorization server delegation
|
|
264
|
+
class ResourceMetadata
|
|
265
|
+
attr_reader :resource, :authorization_servers
|
|
266
|
+
|
|
267
|
+
def initialize(resource:, authorization_servers:)
|
|
268
|
+
@resource = resource
|
|
269
|
+
@authorization_servers = authorization_servers
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Serialize to hash
|
|
273
|
+
# @return [Hash] resource metadata
|
|
274
|
+
def to_h
|
|
275
|
+
{
|
|
276
|
+
resource: @resource,
|
|
277
|
+
authorization_servers: @authorization_servers
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Deserialize from hash
|
|
282
|
+
# @param data [Hash] resource metadata
|
|
283
|
+
# @return [ResourceMetadata] new instance
|
|
284
|
+
def self.from_h(data)
|
|
285
|
+
new(
|
|
286
|
+
resource: data[:resource] || data["resource"],
|
|
287
|
+
authorization_servers: data[:authorization_servers] || data["authorization_servers"]
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Proof Key for Code Exchange (PKCE) implementation (RFC 7636)
|
|
293
|
+
# Required for OAuth 2.1 security
|
|
294
|
+
class PKCE
|
|
295
|
+
attr_reader :code_verifier, :code_challenge, :code_challenge_method
|
|
296
|
+
|
|
297
|
+
def initialize
|
|
298
|
+
@code_verifier = generate_code_verifier
|
|
299
|
+
@code_challenge = generate_code_challenge(@code_verifier)
|
|
300
|
+
@code_challenge_method = "S256" # SHA256 - only secure method for OAuth 2.1
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Serialize to hash
|
|
304
|
+
# @return [Hash] PKCE parameters
|
|
305
|
+
def to_h
|
|
306
|
+
{
|
|
307
|
+
code_verifier: @code_verifier,
|
|
308
|
+
code_challenge: @code_challenge,
|
|
309
|
+
code_challenge_method: @code_challenge_method
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Deserialize from hash
|
|
314
|
+
# @param data [Hash] PKCE data
|
|
315
|
+
# @return [PKCE] new instance
|
|
316
|
+
def self.from_h(data)
|
|
317
|
+
pkce = allocate
|
|
318
|
+
pkce.instance_variable_set(:@code_verifier, data[:code_verifier] || data["code_verifier"])
|
|
319
|
+
pkce.instance_variable_set(:@code_challenge, data[:code_challenge] || data["code_challenge"])
|
|
320
|
+
pkce.instance_variable_set(:@code_challenge_method,
|
|
321
|
+
data[:code_challenge_method] || data["code_challenge_method"] || "S256")
|
|
322
|
+
pkce
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
# Generate cryptographically secure code verifier
|
|
328
|
+
# @return [String] base64url-encoded random bytes
|
|
329
|
+
def generate_code_verifier
|
|
330
|
+
Base64.urlsafe_encode64(SecureRandom.random_bytes(PKCE_VERIFIER_SIZE), padding: false)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Generate code challenge from verifier using SHA256
|
|
334
|
+
# @param verifier [String] code verifier
|
|
335
|
+
# @return [String] base64url-encoded SHA256 hash
|
|
336
|
+
def generate_code_challenge(verifier)
|
|
337
|
+
digest = Digest::SHA256.digest(verifier)
|
|
338
|
+
Base64.urlsafe_encode64(digest, padding: false)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Factory method to create OAuth providers
|
|
343
|
+
# @param server_url [String] OAuth server URL
|
|
344
|
+
# @param type [Symbol] OAuth provider type (:standard or :browser)
|
|
345
|
+
# @param options [Hash] additional options passed to provider
|
|
346
|
+
# @return [OAuthProvider, BrowserOAuthProvider] OAuth provider instance
|
|
347
|
+
def self.create_oauth(server_url, type: :standard, **options)
|
|
348
|
+
case type
|
|
349
|
+
when :browser
|
|
350
|
+
BrowserOAuthProvider.new(server_url: server_url, **options)
|
|
351
|
+
when :standard
|
|
352
|
+
OAuthProvider.new(server_url: server_url, **options)
|
|
353
|
+
else
|
|
354
|
+
raise ArgumentError, "Unknown OAuth type: #{type}. Must be :standard or :browser"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|