ruby-mcp-client 0.6.2 → 0.7.1
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 +4 -4
- data/README.md +316 -10
- data/lib/mcp_client/auth/oauth_provider.rb +514 -0
- data/lib/mcp_client/auth.rb +315 -0
- data/lib/mcp_client/client.rb +1 -1
- data/lib/mcp_client/config_parser.rb +73 -1
- data/lib/mcp_client/http_transport_base.rb +283 -0
- data/lib/mcp_client/json_rpc_common.rb +8 -10
- data/lib/mcp_client/oauth_client.rb +127 -0
- data/lib/mcp_client/server_factory.rb +42 -0
- data/lib/mcp_client/server_http/json_rpc_transport.rb +27 -0
- data/lib/mcp_client/server_http.rb +331 -0
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +5 -5
- data/lib/mcp_client/server_sse.rb +16 -8
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1 -1
- data/lib/mcp_client/server_stdio.rb +1 -1
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +76 -0
- data/lib/mcp_client/server_streamable_http.rb +332 -0
- data/lib/mcp_client/tool.rb +4 -3
- data/lib/mcp_client/version.rb +4 -1
- data/lib/mcp_client.rb +61 -2
- metadata +10 -2
@@ -0,0 +1,514 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
require_relative '../auth'
|
7
|
+
|
8
|
+
module MCPClient
|
9
|
+
module Auth
|
10
|
+
# OAuth 2.1 provider for MCP client authentication
|
11
|
+
# Handles the complete OAuth flow including server discovery, client registration,
|
12
|
+
# authorization, token exchange, and refresh
|
13
|
+
class OAuthProvider
|
14
|
+
# @!attribute [rw] redirect_uri
|
15
|
+
# @return [String] OAuth redirect URI
|
16
|
+
# @!attribute [rw] scope
|
17
|
+
# @return [String, nil] OAuth scope
|
18
|
+
# @!attribute [rw] logger
|
19
|
+
# @return [Logger] Logger instance
|
20
|
+
# @!attribute [rw] storage
|
21
|
+
# @return [Object] Storage backend for tokens and client info
|
22
|
+
# @!attribute [r] server_url
|
23
|
+
# @return [String] The MCP server URL (normalized)
|
24
|
+
attr_accessor :redirect_uri, :scope, :logger, :storage
|
25
|
+
attr_reader :server_url
|
26
|
+
|
27
|
+
# Initialize OAuth provider
|
28
|
+
# @param server_url [String] The MCP server URL (used as OAuth resource parameter)
|
29
|
+
# @param redirect_uri [String] OAuth redirect URI (default: http://localhost:8080/callback)
|
30
|
+
# @param scope [String, nil] OAuth scope
|
31
|
+
# @param logger [Logger, nil] Optional logger
|
32
|
+
# @param storage [Object, nil] Storage backend for tokens and client info
|
33
|
+
def initialize(server_url:, redirect_uri: 'http://localhost:8080/callback', scope: nil, logger: nil, storage: nil)
|
34
|
+
self.server_url = server_url
|
35
|
+
self.redirect_uri = redirect_uri
|
36
|
+
self.scope = scope
|
37
|
+
self.logger = logger || Logger.new($stdout, level: Logger::WARN)
|
38
|
+
self.storage = storage || MemoryStorage.new
|
39
|
+
@http_client = create_http_client
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param url [String] Server URL to normalize
|
43
|
+
def server_url=(url)
|
44
|
+
@server_url = normalize_server_url(url)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get current access token (refresh if needed)
|
48
|
+
# @return [Token, nil] Current valid access token or nil
|
49
|
+
def access_token
|
50
|
+
token = storage.get_token(server_url)
|
51
|
+
logger.debug("OAuth access_token: retrieved token=#{token ? 'present' : 'nil'} for #{server_url}")
|
52
|
+
return nil unless token
|
53
|
+
|
54
|
+
# Return token if still valid
|
55
|
+
return token unless token.expired? || token.expires_soon?
|
56
|
+
|
57
|
+
# Try to refresh if we have a refresh token
|
58
|
+
refresh_token(token) if token.refresh_token
|
59
|
+
end
|
60
|
+
|
61
|
+
# Start OAuth authorization flow
|
62
|
+
# @return [String] Authorization URL to redirect user to
|
63
|
+
# @raise [MCPClient::Errors::ConnectionError] if server discovery fails
|
64
|
+
def start_authorization_flow
|
65
|
+
# Discover authorization server
|
66
|
+
server_metadata = discover_authorization_server
|
67
|
+
|
68
|
+
# Register client if needed
|
69
|
+
client_info = get_or_register_client(server_metadata)
|
70
|
+
|
71
|
+
# Generate PKCE parameters
|
72
|
+
pkce = PKCE.new
|
73
|
+
storage.set_pkce(server_url, pkce)
|
74
|
+
|
75
|
+
# Generate state parameter
|
76
|
+
state = SecureRandom.urlsafe_base64(32)
|
77
|
+
storage.set_state(server_url, state)
|
78
|
+
|
79
|
+
# Build authorization URL
|
80
|
+
build_authorization_url(server_metadata, client_info, pkce, state)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Complete OAuth authorization flow with authorization code
|
84
|
+
# @param code [String] Authorization code from callback
|
85
|
+
# @param state [String] State parameter from callback
|
86
|
+
# @return [Token] Access token
|
87
|
+
# @raise [MCPClient::Errors::ConnectionError] if token exchange fails
|
88
|
+
# @raise [ArgumentError] if state parameter doesn't match
|
89
|
+
def complete_authorization_flow(code, state)
|
90
|
+
# Verify state parameter
|
91
|
+
stored_state = storage.get_state(server_url)
|
92
|
+
raise ArgumentError, 'Invalid state parameter' unless stored_state == state
|
93
|
+
|
94
|
+
# Get stored PKCE and client info
|
95
|
+
pkce = storage.get_pkce(server_url)
|
96
|
+
client_info = storage.get_client_info(server_url)
|
97
|
+
server_metadata = discover_authorization_server
|
98
|
+
|
99
|
+
raise MCPClient::Errors::ConnectionError, 'Missing PKCE or client info' unless pkce && client_info
|
100
|
+
|
101
|
+
# Exchange authorization code for tokens
|
102
|
+
token = exchange_authorization_code(server_metadata, client_info, code, pkce)
|
103
|
+
|
104
|
+
# Store token
|
105
|
+
storage.set_token(server_url, token)
|
106
|
+
|
107
|
+
# Clean up temporary data
|
108
|
+
storage.delete_pkce(server_url)
|
109
|
+
storage.delete_state(server_url)
|
110
|
+
|
111
|
+
token
|
112
|
+
end
|
113
|
+
|
114
|
+
# Apply OAuth authorization to HTTP request
|
115
|
+
# @param request [Faraday::Request] HTTP request to authorize
|
116
|
+
# @return [void]
|
117
|
+
def apply_authorization(request)
|
118
|
+
token = access_token
|
119
|
+
logger.debug("OAuth apply_authorization: token=#{token ? 'present' : 'nil'}")
|
120
|
+
return unless token
|
121
|
+
|
122
|
+
logger.debug("OAuth applying authorization header: #{token.to_header[0..20]}...")
|
123
|
+
request.headers['Authorization'] = token.to_header
|
124
|
+
end
|
125
|
+
|
126
|
+
# Handle 401 Unauthorized response (for server discovery)
|
127
|
+
# @param response [Faraday::Response] HTTP response
|
128
|
+
# @return [ResourceMetadata, nil] Resource metadata if found
|
129
|
+
def handle_unauthorized_response(response)
|
130
|
+
www_authenticate = response.headers['WWW-Authenticate'] || response.headers['www-authenticate']
|
131
|
+
return nil unless www_authenticate
|
132
|
+
|
133
|
+
# Parse WWW-Authenticate header to extract resource metadata URL
|
134
|
+
# Format: Bearer resource="https://example.com/.well-known/oauth-protected-resource"
|
135
|
+
if (match = www_authenticate.match(/resource="([^"]+)"/))
|
136
|
+
resource_metadata_url = match[1]
|
137
|
+
fetch_resource_metadata(resource_metadata_url)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Normalize server URL to canonical form
|
144
|
+
# @param url [String] Server URL
|
145
|
+
# @return [String] Normalized URL
|
146
|
+
def normalize_server_url(url)
|
147
|
+
uri = URI.parse(url)
|
148
|
+
|
149
|
+
# Use lowercase scheme and host
|
150
|
+
uri.scheme = uri.scheme.downcase
|
151
|
+
uri.host = uri.host.downcase
|
152
|
+
|
153
|
+
# Remove default ports
|
154
|
+
uri.port = nil if (uri.scheme == 'http' && uri.port == 80) || (uri.scheme == 'https' && uri.port == 443)
|
155
|
+
|
156
|
+
# Remove trailing slash for empty path or just "/"
|
157
|
+
if uri.path.nil? || uri.path.empty? || uri.path == '/'
|
158
|
+
uri.path = ''
|
159
|
+
elsif uri.path.end_with?('/')
|
160
|
+
uri.path = uri.path.chomp('/')
|
161
|
+
end
|
162
|
+
|
163
|
+
# Remove fragment
|
164
|
+
uri.fragment = nil
|
165
|
+
|
166
|
+
uri.to_s
|
167
|
+
end
|
168
|
+
|
169
|
+
# Create HTTP client for OAuth requests
|
170
|
+
# @return [Faraday::Connection] HTTP client
|
171
|
+
def create_http_client
|
172
|
+
Faraday.new do |f|
|
173
|
+
f.request :retry, max: 3, interval: 1, backoff_factor: 2
|
174
|
+
f.options.timeout = 30
|
175
|
+
f.adapter Faraday.default_adapter
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Build OAuth discovery URL from server URL
|
180
|
+
# Uses only the origin (scheme + host + port) for discovery
|
181
|
+
# @param server_url [String] Full MCP server URL
|
182
|
+
# @return [String] Discovery URL
|
183
|
+
def build_discovery_url(server_url)
|
184
|
+
uri = URI.parse(server_url)
|
185
|
+
|
186
|
+
# Build origin URL (scheme + host + port)
|
187
|
+
origin = "#{uri.scheme}://#{uri.host}"
|
188
|
+
origin += ":#{uri.port}" if uri.port && !default_port?(uri)
|
189
|
+
|
190
|
+
"#{origin}/.well-known/oauth-protected-resource"
|
191
|
+
end
|
192
|
+
|
193
|
+
# Check if URI uses default port for its scheme
|
194
|
+
# @param uri [URI] Parsed URI
|
195
|
+
# @return [Boolean] true if using default port
|
196
|
+
def default_port?(uri)
|
197
|
+
(uri.scheme == 'http' && uri.port == 80) ||
|
198
|
+
(uri.scheme == 'https' && uri.port == 443)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Discover authorization server metadata
|
202
|
+
# @return [ServerMetadata] Authorization server metadata
|
203
|
+
# @raise [MCPClient::Errors::ConnectionError] if discovery fails
|
204
|
+
def discover_authorization_server
|
205
|
+
# Try to get from storage first
|
206
|
+
if (cached = storage.get_server_metadata(server_url))
|
207
|
+
return cached
|
208
|
+
end
|
209
|
+
|
210
|
+
# Build discovery URL using the origin (scheme + host + port) only
|
211
|
+
discovery_url = build_discovery_url(server_url)
|
212
|
+
|
213
|
+
# Fetch resource metadata to find authorization server
|
214
|
+
resource_metadata = fetch_resource_metadata(discovery_url)
|
215
|
+
|
216
|
+
# Get first authorization server
|
217
|
+
auth_server_url = resource_metadata.authorization_servers.first
|
218
|
+
raise MCPClient::Errors::ConnectionError, 'No authorization servers found' unless auth_server_url
|
219
|
+
|
220
|
+
# Fetch authorization server metadata
|
221
|
+
server_metadata = fetch_server_metadata("#{auth_server_url}/.well-known/oauth-authorization-server")
|
222
|
+
|
223
|
+
# Cache the metadata
|
224
|
+
storage.set_server_metadata(server_url, server_metadata)
|
225
|
+
|
226
|
+
server_metadata
|
227
|
+
end
|
228
|
+
|
229
|
+
# Fetch resource metadata from URL
|
230
|
+
# @param url [String] Resource metadata URL
|
231
|
+
# @return [ResourceMetadata] Resource metadata
|
232
|
+
# @raise [MCPClient::Errors::ConnectionError] if fetch fails
|
233
|
+
def fetch_resource_metadata(url)
|
234
|
+
logger.debug("Fetching resource metadata from: #{url}")
|
235
|
+
|
236
|
+
response = @http_client.get(url) do |req|
|
237
|
+
req.headers['Accept'] = 'application/json'
|
238
|
+
end
|
239
|
+
|
240
|
+
unless response.success?
|
241
|
+
raise MCPClient::Errors::ConnectionError, "Failed to fetch resource metadata: HTTP #{response.status}"
|
242
|
+
end
|
243
|
+
|
244
|
+
data = JSON.parse(response.body)
|
245
|
+
ResourceMetadata.from_h(data)
|
246
|
+
rescue JSON::ParserError => e
|
247
|
+
raise MCPClient::Errors::ConnectionError, "Invalid resource metadata JSON: #{e.message}"
|
248
|
+
rescue Faraday::Error => e
|
249
|
+
raise MCPClient::Errors::ConnectionError, "Network error fetching resource metadata: #{e.message}"
|
250
|
+
end
|
251
|
+
|
252
|
+
# Fetch authorization server metadata from URL
|
253
|
+
# @param url [String] Server metadata URL
|
254
|
+
# @return [ServerMetadata] Server metadata
|
255
|
+
# @raise [MCPClient::Errors::ConnectionError] if fetch fails
|
256
|
+
def fetch_server_metadata(url)
|
257
|
+
logger.debug("Fetching server metadata from: #{url}")
|
258
|
+
|
259
|
+
response = @http_client.get(url) do |req|
|
260
|
+
req.headers['Accept'] = 'application/json'
|
261
|
+
end
|
262
|
+
|
263
|
+
unless response.success?
|
264
|
+
raise MCPClient::Errors::ConnectionError, "Failed to fetch server metadata: HTTP #{response.status}"
|
265
|
+
end
|
266
|
+
|
267
|
+
data = JSON.parse(response.body)
|
268
|
+
ServerMetadata.from_h(data)
|
269
|
+
rescue JSON::ParserError => e
|
270
|
+
raise MCPClient::Errors::ConnectionError, "Invalid server metadata JSON: #{e.message}"
|
271
|
+
rescue Faraday::Error => e
|
272
|
+
raise MCPClient::Errors::ConnectionError, "Network error fetching server metadata: #{e.message}"
|
273
|
+
end
|
274
|
+
|
275
|
+
# Get or register OAuth client
|
276
|
+
# @param server_metadata [ServerMetadata] Authorization server metadata
|
277
|
+
# @return [ClientInfo] Client information
|
278
|
+
# @raise [MCPClient::Errors::ConnectionError] if registration fails
|
279
|
+
def get_or_register_client(server_metadata)
|
280
|
+
# Try to get existing client info from storage
|
281
|
+
if (client_info = storage.get_client_info(server_url)) && !client_info.client_secret_expired?
|
282
|
+
return client_info
|
283
|
+
end
|
284
|
+
|
285
|
+
# Register new client if server supports it
|
286
|
+
if server_metadata.supports_registration?
|
287
|
+
register_client(server_metadata)
|
288
|
+
else
|
289
|
+
raise MCPClient::Errors::ConnectionError,
|
290
|
+
'Dynamic client registration not supported and no client credentials found'
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Register OAuth client dynamically
|
295
|
+
# @param server_metadata [ServerMetadata] Authorization server metadata
|
296
|
+
# @return [ClientInfo] Registered client information
|
297
|
+
# @raise [MCPClient::Errors::ConnectionError] if registration fails
|
298
|
+
def register_client(server_metadata)
|
299
|
+
logger.debug("Registering OAuth client at: #{server_metadata.registration_endpoint}")
|
300
|
+
|
301
|
+
metadata = ClientMetadata.new(
|
302
|
+
redirect_uris: [redirect_uri],
|
303
|
+
token_endpoint_auth_method: 'none', # Public client
|
304
|
+
grant_types: %w[authorization_code refresh_token],
|
305
|
+
response_types: ['code'],
|
306
|
+
scope: scope
|
307
|
+
)
|
308
|
+
|
309
|
+
response = @http_client.post(server_metadata.registration_endpoint) do |req|
|
310
|
+
req.headers['Content-Type'] = 'application/json'
|
311
|
+
req.headers['Accept'] = 'application/json'
|
312
|
+
req.body = metadata.to_h.to_json
|
313
|
+
end
|
314
|
+
|
315
|
+
unless response.success?
|
316
|
+
raise MCPClient::Errors::ConnectionError, "Client registration failed: HTTP #{response.status}"
|
317
|
+
end
|
318
|
+
|
319
|
+
data = JSON.parse(response.body)
|
320
|
+
client_info = ClientInfo.new(
|
321
|
+
client_id: data['client_id'],
|
322
|
+
client_secret: data['client_secret'],
|
323
|
+
client_id_issued_at: data['client_id_issued_at'],
|
324
|
+
client_secret_expires_at: data['client_secret_expires_at'],
|
325
|
+
metadata: metadata
|
326
|
+
)
|
327
|
+
|
328
|
+
# Store client info
|
329
|
+
storage.set_client_info(server_url, client_info)
|
330
|
+
|
331
|
+
client_info
|
332
|
+
rescue JSON::ParserError => e
|
333
|
+
raise MCPClient::Errors::ConnectionError, "Invalid client registration response: #{e.message}"
|
334
|
+
rescue Faraday::Error => e
|
335
|
+
raise MCPClient::Errors::ConnectionError, "Network error during client registration: #{e.message}"
|
336
|
+
end
|
337
|
+
|
338
|
+
# Build authorization URL
|
339
|
+
# @param server_metadata [ServerMetadata] Server metadata
|
340
|
+
# @param client_info [ClientInfo] Client information
|
341
|
+
# @param pkce [PKCE] PKCE parameters
|
342
|
+
# @param state [String] State parameter
|
343
|
+
# @return [String] Authorization URL
|
344
|
+
def build_authorization_url(server_metadata, client_info, pkce, state)
|
345
|
+
params = {
|
346
|
+
response_type: 'code',
|
347
|
+
client_id: client_info.client_id,
|
348
|
+
redirect_uri: redirect_uri,
|
349
|
+
scope: scope,
|
350
|
+
state: state,
|
351
|
+
code_challenge: pkce.code_challenge,
|
352
|
+
code_challenge_method: pkce.code_challenge_method,
|
353
|
+
resource: server_url
|
354
|
+
}.compact
|
355
|
+
|
356
|
+
uri = URI.parse(server_metadata.authorization_endpoint)
|
357
|
+
uri.query = URI.encode_www_form(params)
|
358
|
+
uri.to_s
|
359
|
+
end
|
360
|
+
|
361
|
+
# Exchange authorization code for access token
|
362
|
+
# @param server_metadata [ServerMetadata] Server metadata
|
363
|
+
# @param client_info [ClientInfo] Client information
|
364
|
+
# @param code [String] Authorization code
|
365
|
+
# @param pkce [PKCE] PKCE parameters
|
366
|
+
# @return [Token] Access token
|
367
|
+
# @raise [MCPClient::Errors::ConnectionError] if token exchange fails
|
368
|
+
def exchange_authorization_code(server_metadata, client_info, code, pkce)
|
369
|
+
logger.debug("Exchanging authorization code for token at: #{server_metadata.token_endpoint}")
|
370
|
+
|
371
|
+
params = {
|
372
|
+
grant_type: 'authorization_code',
|
373
|
+
code: code,
|
374
|
+
redirect_uri: redirect_uri,
|
375
|
+
client_id: client_info.client_id,
|
376
|
+
code_verifier: pkce.code_verifier,
|
377
|
+
resource: server_url
|
378
|
+
}
|
379
|
+
|
380
|
+
response = @http_client.post(server_metadata.token_endpoint) do |req|
|
381
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
382
|
+
req.headers['Accept'] = 'application/json'
|
383
|
+
req.body = URI.encode_www_form(params)
|
384
|
+
end
|
385
|
+
|
386
|
+
unless response.success?
|
387
|
+
raise MCPClient::Errors::ConnectionError, "Token exchange failed: HTTP #{response.status} - #{response.body}"
|
388
|
+
end
|
389
|
+
|
390
|
+
data = JSON.parse(response.body)
|
391
|
+
Token.new(
|
392
|
+
access_token: data['access_token'],
|
393
|
+
token_type: data['token_type'] || 'Bearer',
|
394
|
+
expires_in: data['expires_in'],
|
395
|
+
scope: data['scope'],
|
396
|
+
refresh_token: data['refresh_token']
|
397
|
+
)
|
398
|
+
rescue JSON::ParserError => e
|
399
|
+
raise MCPClient::Errors::ConnectionError, "Invalid token response: #{e.message}"
|
400
|
+
rescue Faraday::Error => e
|
401
|
+
raise MCPClient::Errors::ConnectionError, "Network error during token exchange: #{e.message}"
|
402
|
+
end
|
403
|
+
|
404
|
+
# Refresh access token
|
405
|
+
# @param token [Token] Current token with refresh token
|
406
|
+
# @return [Token, nil] New access token or nil if refresh failed
|
407
|
+
def refresh_token(token)
|
408
|
+
return nil unless token.refresh_token
|
409
|
+
|
410
|
+
logger.debug('Refreshing access token')
|
411
|
+
|
412
|
+
server_metadata = discover_authorization_server
|
413
|
+
client_info = storage.get_client_info(server_url)
|
414
|
+
|
415
|
+
return nil unless server_metadata && client_info
|
416
|
+
|
417
|
+
params = {
|
418
|
+
grant_type: 'refresh_token',
|
419
|
+
refresh_token: token.refresh_token,
|
420
|
+
client_id: client_info.client_id,
|
421
|
+
resource: server_url
|
422
|
+
}
|
423
|
+
|
424
|
+
response = @http_client.post(server_metadata.token_endpoint) do |req|
|
425
|
+
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
426
|
+
req.headers['Accept'] = 'application/json'
|
427
|
+
req.body = URI.encode_www_form(params)
|
428
|
+
end
|
429
|
+
|
430
|
+
unless response.success?
|
431
|
+
logger.warn("Token refresh failed: HTTP #{response.status}")
|
432
|
+
return nil
|
433
|
+
end
|
434
|
+
|
435
|
+
data = JSON.parse(response.body)
|
436
|
+
new_token = Token.new(
|
437
|
+
access_token: data['access_token'],
|
438
|
+
token_type: data['token_type'] || 'Bearer',
|
439
|
+
expires_in: data['expires_in'],
|
440
|
+
scope: data['scope'],
|
441
|
+
refresh_token: data['refresh_token'] || token.refresh_token
|
442
|
+
)
|
443
|
+
|
444
|
+
storage.set_token(server_url, new_token)
|
445
|
+
new_token
|
446
|
+
rescue JSON::ParserError => e
|
447
|
+
logger.warn("Invalid token refresh response: #{e.message}")
|
448
|
+
nil
|
449
|
+
rescue Faraday::Error => e
|
450
|
+
logger.warn("Network error during token refresh: #{e.message}")
|
451
|
+
nil
|
452
|
+
end
|
453
|
+
|
454
|
+
# Simple in-memory storage for OAuth data
|
455
|
+
class MemoryStorage
|
456
|
+
def initialize
|
457
|
+
@tokens = {}
|
458
|
+
@client_infos = {}
|
459
|
+
@server_metadata = {}
|
460
|
+
@pkce_data = {}
|
461
|
+
@state_data = {}
|
462
|
+
end
|
463
|
+
|
464
|
+
def get_token(server_url)
|
465
|
+
@tokens[server_url]
|
466
|
+
end
|
467
|
+
|
468
|
+
def set_token(server_url, token)
|
469
|
+
@tokens[server_url] = token
|
470
|
+
end
|
471
|
+
|
472
|
+
def get_client_info(server_url)
|
473
|
+
@client_infos[server_url]
|
474
|
+
end
|
475
|
+
|
476
|
+
def set_client_info(server_url, client_info)
|
477
|
+
@client_infos[server_url] = client_info
|
478
|
+
end
|
479
|
+
|
480
|
+
def get_server_metadata(server_url)
|
481
|
+
@server_metadata[server_url]
|
482
|
+
end
|
483
|
+
|
484
|
+
def set_server_metadata(server_url, metadata)
|
485
|
+
@server_metadata[server_url] = metadata
|
486
|
+
end
|
487
|
+
|
488
|
+
def get_pkce(server_url)
|
489
|
+
@pkce_data[server_url]
|
490
|
+
end
|
491
|
+
|
492
|
+
def set_pkce(server_url, pkce)
|
493
|
+
@pkce_data[server_url] = pkce
|
494
|
+
end
|
495
|
+
|
496
|
+
def delete_pkce(server_url)
|
497
|
+
@pkce_data.delete(server_url)
|
498
|
+
end
|
499
|
+
|
500
|
+
def get_state(server_url)
|
501
|
+
@state_data[server_url]
|
502
|
+
end
|
503
|
+
|
504
|
+
def set_state(server_url, state)
|
505
|
+
@state_data[server_url] = state
|
506
|
+
end
|
507
|
+
|
508
|
+
def delete_state(server_url)
|
509
|
+
@state_data.delete(server_url)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|