actionmcp 0.72.0 → 0.80.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 +4 -4
- data/README.md +1 -1
- data/app/controllers/action_mcp/application_controller.rb +20 -12
- data/app/models/action_mcp/session/message.rb +31 -20
- data/app/models/action_mcp/session/resource.rb +35 -20
- data/app/models/action_mcp/session/sse_event.rb +23 -17
- data/app/models/action_mcp/session/subscription.rb +22 -15
- data/app/models/action_mcp/session.rb +42 -119
- data/config/routes.rb +0 -13
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/client/streamable_http_transport.rb +1 -46
- data/lib/action_mcp/client.rb +2 -25
- data/lib/action_mcp/configuration.rb +51 -24
- data/lib/action_mcp/engine.rb +0 -7
- data/lib/action_mcp/filtered_logger.rb +2 -6
- data/lib/action_mcp/gateway_identifier.rb +187 -3
- data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
- data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
- data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
- data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
- data/lib/action_mcp/gateway_identifiers.rb +26 -0
- data/lib/action_mcp/server/base_session.rb +2 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +1 -6
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
- data/lib/generators/action_mcp/install/install_generator.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +80 -31
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- metadata +13 -97
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -265
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -125
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -201
- data/app/models/action_mcp/oauth_client.rb +0 -159
- data/app/models/action_mcp/oauth_token.rb +0 -142
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -28
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -44
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -39
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -135
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
- data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
- data/lib/action_mcp/jwt_decoder.rb +0 -28
- data/lib/action_mcp/jwt_identifier.rb +0 -28
- data/lib/action_mcp/none_identifier.rb +0 -19
- data/lib/action_mcp/o_auth_identifier.rb +0 -34
- data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
- data/lib/action_mcp/oauth/error.rb +0 -79
- data/lib/action_mcp/oauth/memory_storage.rb +0 -132
- data/lib/action_mcp/oauth/middleware.rb +0 -128
- data/lib/action_mcp/oauth/provider.rb +0 -406
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -162
@@ -1,265 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "ostruct"
|
4
|
-
|
5
|
-
module ActionMCP
|
6
|
-
module OAuth
|
7
|
-
# OAuth 2.1 endpoints controller
|
8
|
-
# Handles authorization, token, introspection, and revocation endpoints
|
9
|
-
class EndpointsController < ActionController::Base
|
10
|
-
protect_from_forgery with: :null_session
|
11
|
-
before_action :check_oauth_enabled
|
12
|
-
|
13
|
-
# GET /oauth/authorize
|
14
|
-
# Authorization endpoint for OAuth 2.1 authorization code flow
|
15
|
-
def authorize
|
16
|
-
# Extract parameters
|
17
|
-
client_id = params[:client_id]
|
18
|
-
redirect_uri = params[:redirect_uri]
|
19
|
-
response_type = params[:response_type]
|
20
|
-
scope = params[:scope]
|
21
|
-
state = params[:state]
|
22
|
-
code_challenge = params[:code_challenge]
|
23
|
-
code_challenge_method = params[:code_challenge_method]
|
24
|
-
|
25
|
-
# Validate required parameters
|
26
|
-
if client_id.blank? || redirect_uri.blank? || response_type.blank?
|
27
|
-
return render_error("invalid_request", "Missing required parameters")
|
28
|
-
end
|
29
|
-
|
30
|
-
# Validate response type
|
31
|
-
unless response_type == "code"
|
32
|
-
return render_error("unsupported_response_type", "Only authorization code flow supported")
|
33
|
-
end
|
34
|
-
|
35
|
-
# Validate PKCE if required
|
36
|
-
if oauth_config["pkce_required"] && code_challenge.blank?
|
37
|
-
return render_error("invalid_request", "PKCE required")
|
38
|
-
end
|
39
|
-
|
40
|
-
# In a real implementation, this would show a consent page
|
41
|
-
# For now, we'll auto-approve for configured clients
|
42
|
-
if auto_approve_client?(client_id)
|
43
|
-
# Generate authorization code
|
44
|
-
user_id = current_user&.id || "anonymous"
|
45
|
-
|
46
|
-
begin
|
47
|
-
code = ActionMCP::OAuth::Provider.generate_authorization_code(
|
48
|
-
client_id: client_id,
|
49
|
-
redirect_uri: redirect_uri,
|
50
|
-
scope: scope || default_scope,
|
51
|
-
code_challenge: code_challenge,
|
52
|
-
code_challenge_method: code_challenge_method,
|
53
|
-
user_id: user_id
|
54
|
-
)
|
55
|
-
|
56
|
-
# Redirect back to client with authorization code
|
57
|
-
redirect_params = { code: code }
|
58
|
-
redirect_params[:state] = state if state
|
59
|
-
redirect_to "#{redirect_uri}?#{redirect_params.to_query}", allow_other_host: true
|
60
|
-
rescue ActionMCP::OAuth::Error => e
|
61
|
-
render_error(e.oauth_error_code, e.message)
|
62
|
-
end
|
63
|
-
else
|
64
|
-
# In production, show consent page
|
65
|
-
render_consent_page(client_id, redirect_uri, scope, state, code_challenge, code_challenge_method)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# POST /oauth/token
|
70
|
-
# Token endpoint for exchanging authorization codes and refreshing tokens
|
71
|
-
def token
|
72
|
-
grant_type = params[:grant_type]
|
73
|
-
|
74
|
-
case grant_type
|
75
|
-
when "authorization_code"
|
76
|
-
handle_authorization_code_grant
|
77
|
-
when "refresh_token"
|
78
|
-
handle_refresh_token_grant
|
79
|
-
when "client_credentials"
|
80
|
-
handle_client_credentials_grant
|
81
|
-
else
|
82
|
-
render_token_error("unsupported_grant_type", "Unsupported grant type")
|
83
|
-
end
|
84
|
-
rescue ActionMCP::OAuth::Error => e
|
85
|
-
render_token_error(e.oauth_error_code, e.message)
|
86
|
-
end
|
87
|
-
|
88
|
-
# POST /oauth/introspect
|
89
|
-
# Token introspection endpoint (RFC 7662)
|
90
|
-
def introspect
|
91
|
-
token = params[:token]
|
92
|
-
return render_introspection_error unless token
|
93
|
-
|
94
|
-
# Authenticate client for introspection
|
95
|
-
client_id, = extract_client_credentials
|
96
|
-
return render_introspection_error unless client_id
|
97
|
-
|
98
|
-
begin
|
99
|
-
token_info = ActionMCP::OAuth::Provider.introspect_token(token)
|
100
|
-
render json: token_info
|
101
|
-
rescue ActionMCP::OAuth::Error
|
102
|
-
render json: { active: false }
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# POST /oauth/revoke
|
107
|
-
# Token revocation endpoint (RFC 7009)
|
108
|
-
def revoke
|
109
|
-
token = params[:token]
|
110
|
-
token_type_hint = params[:token_type_hint]
|
111
|
-
|
112
|
-
return head :bad_request unless token
|
113
|
-
|
114
|
-
# Authenticate client
|
115
|
-
client_id, = extract_client_credentials
|
116
|
-
return head :unauthorized unless client_id
|
117
|
-
|
118
|
-
begin
|
119
|
-
ActionMCP::OAuth::Provider.revoke_token(token, token_type_hint: token_type_hint)
|
120
|
-
head :ok
|
121
|
-
rescue ActionMCP::OAuth::Error
|
122
|
-
head :bad_request
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
private
|
127
|
-
|
128
|
-
def check_oauth_enabled
|
129
|
-
auth_methods = ActionMCP.configuration.authentication_methods
|
130
|
-
return if auth_methods&.include?("oauth")
|
131
|
-
|
132
|
-
head :not_found
|
133
|
-
end
|
134
|
-
|
135
|
-
def oauth_config
|
136
|
-
@oauth_config ||= ActionMCP.configuration.oauth_config || {}
|
137
|
-
end
|
138
|
-
|
139
|
-
def handle_authorization_code_grant
|
140
|
-
code = params[:code]
|
141
|
-
client_id = params[:client_id]
|
142
|
-
client_secret = params[:client_secret]
|
143
|
-
redirect_uri = params[:redirect_uri]
|
144
|
-
code_verifier = params[:code_verifier]
|
145
|
-
|
146
|
-
# Extract client credentials from Authorization header if not in params
|
147
|
-
client_id, client_secret = extract_client_credentials if client_id.blank?
|
148
|
-
|
149
|
-
return render_token_error("invalid_request", "Missing required parameters") if code.blank? || client_id.blank?
|
150
|
-
|
151
|
-
token_response = ActionMCP::OAuth::Provider.exchange_code_for_token(
|
152
|
-
code: code,
|
153
|
-
client_id: client_id,
|
154
|
-
client_secret: client_secret,
|
155
|
-
redirect_uri: redirect_uri,
|
156
|
-
code_verifier: code_verifier
|
157
|
-
)
|
158
|
-
|
159
|
-
render json: token_response
|
160
|
-
end
|
161
|
-
|
162
|
-
def handle_refresh_token_grant
|
163
|
-
refresh_token = params[:refresh_token]
|
164
|
-
scope = params[:scope]
|
165
|
-
|
166
|
-
# Extract client credentials
|
167
|
-
client_id, client_secret = extract_client_credentials
|
168
|
-
client_id ||= params[:client_id]
|
169
|
-
client_secret ||= params[:client_secret]
|
170
|
-
|
171
|
-
if refresh_token.blank? || client_id.blank?
|
172
|
-
return render_token_error("invalid_request",
|
173
|
-
"Missing required parameters")
|
174
|
-
end
|
175
|
-
|
176
|
-
token_response = ActionMCP::OAuth::Provider.refresh_access_token(
|
177
|
-
refresh_token: refresh_token,
|
178
|
-
client_id: client_id,
|
179
|
-
client_secret: client_secret,
|
180
|
-
scope: scope
|
181
|
-
)
|
182
|
-
|
183
|
-
render json: token_response
|
184
|
-
end
|
185
|
-
|
186
|
-
def handle_client_credentials_grant
|
187
|
-
scope = params[:scope]
|
188
|
-
|
189
|
-
# Extract client credentials
|
190
|
-
client_id, client_secret = extract_client_credentials
|
191
|
-
client_id ||= params[:client_id]
|
192
|
-
client_secret ||= params[:client_secret]
|
193
|
-
|
194
|
-
return render_token_error("invalid_request", "Missing client credentials") if client_id.blank?
|
195
|
-
|
196
|
-
token_response = ActionMCP::OAuth::Provider.client_credentials_grant(
|
197
|
-
client_id: client_id,
|
198
|
-
client_secret: client_secret,
|
199
|
-
scope: scope
|
200
|
-
)
|
201
|
-
|
202
|
-
render json: token_response
|
203
|
-
end
|
204
|
-
|
205
|
-
def extract_client_credentials
|
206
|
-
auth_header = request.headers["Authorization"]
|
207
|
-
if auth_header&.start_with?("Basic ")
|
208
|
-
encoded = auth_header.split(" ", 2).last
|
209
|
-
decoded = Base64.decode64(encoded)
|
210
|
-
decoded.split(":", 2)
|
211
|
-
else
|
212
|
-
[ nil, nil ]
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
def auto_approve_client?(client_id)
|
217
|
-
# In development/testing, auto-approve known clients
|
218
|
-
# In production, this should check a proper client registry
|
219
|
-
Rails.env.development? || Rails.env.test? || oauth_config["auto_approve_clients"]&.include?(client_id)
|
220
|
-
end
|
221
|
-
|
222
|
-
def current_user
|
223
|
-
# This should be implemented by the application
|
224
|
-
# For now, return a default user for development
|
225
|
-
if Rails.env.development? || Rails.env.test?
|
226
|
-
OpenStruct.new(id: "dev_user", email: "dev@example.com")
|
227
|
-
else
|
228
|
-
# In production, this should integrate with your authentication system
|
229
|
-
nil
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
def default_scope
|
234
|
-
oauth_config["default_scope"] || "mcp:tools mcp:resources mcp:prompts"
|
235
|
-
end
|
236
|
-
|
237
|
-
def render_error(error_code, description)
|
238
|
-
render json: {
|
239
|
-
error: error_code,
|
240
|
-
error_description: description
|
241
|
-
}, status: :bad_request
|
242
|
-
end
|
243
|
-
|
244
|
-
def render_token_error(error_code, description)
|
245
|
-
render json: {
|
246
|
-
error: error_code,
|
247
|
-
error_description: description
|
248
|
-
}, status: :bad_request
|
249
|
-
end
|
250
|
-
|
251
|
-
def render_introspection_error
|
252
|
-
render json: { active: false }, status: :bad_request
|
253
|
-
end
|
254
|
-
|
255
|
-
def render_consent_page(_client_id, _redirect_uri, _scope, _state, _code_challenge, _code_challenge_method)
|
256
|
-
# In production, this would render a proper consent page
|
257
|
-
# For now, just auto-deny unknown clients
|
258
|
-
render json: {
|
259
|
-
error: "access_denied",
|
260
|
-
error_description: "User denied authorization"
|
261
|
-
}, status: :forbidden
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
@@ -1,125 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
module OAuth
|
5
|
-
# Controller for OAuth 2.1 metadata endpoints
|
6
|
-
# Provides server discovery information as per RFC 8414
|
7
|
-
class MetadataController < ActionController::Base
|
8
|
-
before_action :check_oauth_enabled
|
9
|
-
|
10
|
-
# GET /.well-known/oauth-authorization-server
|
11
|
-
# Returns OAuth Authorization Server Metadata as per RFC 8414
|
12
|
-
def authorization_server
|
13
|
-
metadata = {
|
14
|
-
issuer: issuer_url,
|
15
|
-
authorization_endpoint: authorization_endpoint,
|
16
|
-
token_endpoint: token_endpoint,
|
17
|
-
introspection_endpoint: introspection_endpoint,
|
18
|
-
revocation_endpoint: revocation_endpoint,
|
19
|
-
response_types_supported: response_types_supported,
|
20
|
-
grant_types_supported: grant_types_supported,
|
21
|
-
token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
|
22
|
-
scopes_supported: scopes_supported,
|
23
|
-
code_challenge_methods_supported: code_challenge_methods_supported,
|
24
|
-
service_documentation: service_documentation
|
25
|
-
}
|
26
|
-
|
27
|
-
# Add optional fields based on configuration
|
28
|
-
metadata[:registration_endpoint] = registration_endpoint if oauth_config[:enable_dynamic_registration]
|
29
|
-
|
30
|
-
metadata[:jwks_uri] = oauth_config[:jwks_uri] if oauth_config[:jwks_uri]
|
31
|
-
|
32
|
-
render json: metadata
|
33
|
-
end
|
34
|
-
|
35
|
-
# GET /.well-known/oauth-protected-resource
|
36
|
-
# Returns Protected Resource Metadata as per RFC 8705
|
37
|
-
def protected_resource
|
38
|
-
metadata = {
|
39
|
-
resource: issuer_url,
|
40
|
-
authorization_servers: [ issuer_url ],
|
41
|
-
scopes_supported: scopes_supported,
|
42
|
-
bearer_methods_supported: [ "header" ],
|
43
|
-
resource_documentation: resource_documentation
|
44
|
-
}
|
45
|
-
|
46
|
-
render json: metadata
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def check_oauth_enabled
|
52
|
-
auth_methods = ActionMCP.configuration.authentication_methods
|
53
|
-
return if auth_methods&.include?("oauth")
|
54
|
-
|
55
|
-
head :not_found
|
56
|
-
end
|
57
|
-
|
58
|
-
def oauth_config
|
59
|
-
@oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
|
60
|
-
end
|
61
|
-
|
62
|
-
def issuer_url
|
63
|
-
@issuer_url ||= oauth_config.fetch(:issuer_url, request.base_url)
|
64
|
-
end
|
65
|
-
|
66
|
-
def authorization_endpoint
|
67
|
-
"#{issuer_url}/oauth/authorize"
|
68
|
-
end
|
69
|
-
|
70
|
-
def token_endpoint
|
71
|
-
"#{issuer_url}/oauth/token"
|
72
|
-
end
|
73
|
-
|
74
|
-
def introspection_endpoint
|
75
|
-
"#{issuer_url}/oauth/introspect"
|
76
|
-
end
|
77
|
-
|
78
|
-
def revocation_endpoint
|
79
|
-
"#{issuer_url}/oauth/revoke"
|
80
|
-
end
|
81
|
-
|
82
|
-
def registration_endpoint
|
83
|
-
"#{issuer_url}/oauth/register"
|
84
|
-
end
|
85
|
-
|
86
|
-
def response_types_supported
|
87
|
-
[ "code" ]
|
88
|
-
end
|
89
|
-
|
90
|
-
def grant_types_supported
|
91
|
-
grants = [ "authorization_code" ]
|
92
|
-
grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
|
93
|
-
grants << "client_credentials" if oauth_config[:enable_client_credentials]
|
94
|
-
grants
|
95
|
-
end
|
96
|
-
|
97
|
-
def token_endpoint_auth_methods_supported
|
98
|
-
methods = %w[client_secret_basic client_secret_post]
|
99
|
-
methods << "none" if oauth_config[:allow_public_clients]
|
100
|
-
methods
|
101
|
-
end
|
102
|
-
|
103
|
-
def scopes_supported
|
104
|
-
oauth_config.fetch(:scopes_supported, [ "mcp:tools", "mcp:resources", "mcp:prompts" ])
|
105
|
-
end
|
106
|
-
|
107
|
-
def code_challenge_methods_supported
|
108
|
-
methods = []
|
109
|
-
if oauth_config[:pkce_required] || oauth_config[:pkce_supported]
|
110
|
-
methods << "S256"
|
111
|
-
methods << "plain" if oauth_config[:allow_plain_pkce]
|
112
|
-
end
|
113
|
-
methods
|
114
|
-
end
|
115
|
-
|
116
|
-
def service_documentation
|
117
|
-
oauth_config.fetch(:service_documentation, "#{request.base_url}/docs")
|
118
|
-
end
|
119
|
-
|
120
|
-
def resource_documentation
|
121
|
-
oauth_config.fetch(:resource_documentation, "#{request.base_url}/docs/api")
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
@@ -1,201 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
module OAuth
|
5
|
-
# OAuth 2.0 Dynamic Client Registration Controller (RFC 7591)
|
6
|
-
# Allows clients to dynamically register with the authorization server
|
7
|
-
class RegistrationController < ActionController::Base
|
8
|
-
protect_from_forgery with: :null_session
|
9
|
-
before_action :check_oauth_enabled
|
10
|
-
before_action :check_registration_enabled
|
11
|
-
|
12
|
-
# POST /oauth/register
|
13
|
-
# Dynamic client registration endpoint as per RFC 7591
|
14
|
-
def create
|
15
|
-
# Parse client metadata from request body
|
16
|
-
client_metadata = parse_client_metadata
|
17
|
-
|
18
|
-
# Validate required fields
|
19
|
-
validate_client_metadata(client_metadata)
|
20
|
-
|
21
|
-
# Generate client credentials
|
22
|
-
client_id = generate_client_id
|
23
|
-
client_secret = nil # Public clients by default
|
24
|
-
|
25
|
-
# Generate client secret for confidential clients
|
26
|
-
client_secret = generate_client_secret if client_metadata["token_endpoint_auth_method"] != "none"
|
27
|
-
|
28
|
-
# Store client registration
|
29
|
-
client_info = {
|
30
|
-
client_id: client_id,
|
31
|
-
client_secret: client_secret,
|
32
|
-
client_id_issued_at: Time.current.to_i,
|
33
|
-
client_metadata: client_metadata,
|
34
|
-
created_at: Time.current
|
35
|
-
}
|
36
|
-
|
37
|
-
# Save client registration (delegated to provider)
|
38
|
-
ActionMCP::OAuth::Provider.register_client(client_info)
|
39
|
-
|
40
|
-
# Build response according to RFC 7591
|
41
|
-
response_data = {
|
42
|
-
client_id: client_id,
|
43
|
-
client_id_issued_at: client_info[:client_id_issued_at]
|
44
|
-
}
|
45
|
-
|
46
|
-
# Include client secret for confidential clients
|
47
|
-
if client_secret
|
48
|
-
response_data[:client_secret] = client_secret
|
49
|
-
response_data[:client_secret_expires_at] = 0 # Never expires
|
50
|
-
end
|
51
|
-
|
52
|
-
# Include all client metadata in response
|
53
|
-
response_data.merge!(client_metadata)
|
54
|
-
|
55
|
-
# Add registration management fields if enabled
|
56
|
-
if oauth_config[:enable_registration_management]
|
57
|
-
response_data[:registration_access_token] = generate_registration_access_token(client_id)
|
58
|
-
response_data[:registration_client_uri] = registration_client_url(client_id)
|
59
|
-
end
|
60
|
-
|
61
|
-
render json: response_data, status: :created
|
62
|
-
rescue ActionMCP::OAuth::Error => e
|
63
|
-
render_registration_error(e.oauth_error_code, e.message)
|
64
|
-
rescue StandardError => e
|
65
|
-
Rails.logger.error "Registration error: #{e.message}"
|
66
|
-
render_registration_error("invalid_client_metadata", "Invalid client metadata")
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def check_oauth_enabled
|
72
|
-
auth_methods = ActionMCP.configuration.authentication_methods
|
73
|
-
return if auth_methods&.include?("oauth")
|
74
|
-
|
75
|
-
head :not_found
|
76
|
-
end
|
77
|
-
|
78
|
-
def check_registration_enabled
|
79
|
-
return if oauth_config[:enable_dynamic_registration]
|
80
|
-
|
81
|
-
head :not_found
|
82
|
-
end
|
83
|
-
|
84
|
-
def oauth_config
|
85
|
-
@oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
|
86
|
-
end
|
87
|
-
|
88
|
-
def parse_client_metadata
|
89
|
-
# RFC 7591 requires JSON request body
|
90
|
-
unless request.content_type&.include?("application/json")
|
91
|
-
raise ActionMCP::OAuth::InvalidRequestError, "Content-Type must be application/json"
|
92
|
-
end
|
93
|
-
|
94
|
-
JSON.parse(request.body.read)
|
95
|
-
rescue JSON::ParserError
|
96
|
-
raise ActionMCP::OAuth::InvalidRequestError, "Invalid JSON"
|
97
|
-
end
|
98
|
-
|
99
|
-
def validate_client_metadata(metadata)
|
100
|
-
# Validate redirect URIs (required for authorization code flow)
|
101
|
-
if metadata["grant_types"]&.include?("authorization_code") ||
|
102
|
-
metadata["response_types"]&.include?("code")
|
103
|
-
unless metadata["redirect_uris"].is_a?(Array) && metadata["redirect_uris"].any?
|
104
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "redirect_uris required for authorization code flow"
|
105
|
-
end
|
106
|
-
|
107
|
-
# Validate redirect URI format
|
108
|
-
metadata["redirect_uris"].each do |uri|
|
109
|
-
validate_redirect_uri(uri)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# Validate grant types
|
114
|
-
if metadata["grant_types"]
|
115
|
-
unsupported = metadata["grant_types"] - supported_grant_types
|
116
|
-
if unsupported.any?
|
117
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported grant types: #{unsupported.join(', ')}"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
# Validate response types
|
122
|
-
if metadata["response_types"]
|
123
|
-
unsupported = metadata["response_types"] - supported_response_types
|
124
|
-
if unsupported.any?
|
125
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported response types: #{unsupported.join(', ')}"
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Validate token endpoint auth method
|
130
|
-
return unless metadata["token_endpoint_auth_method"]
|
131
|
-
return if supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
|
132
|
-
|
133
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
|
134
|
-
end
|
135
|
-
|
136
|
-
def validate_redirect_uri(uri)
|
137
|
-
parsed = URI.parse(uri)
|
138
|
-
|
139
|
-
# Must be absolute URI
|
140
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute" unless parsed.absolute?
|
141
|
-
|
142
|
-
# For non-localhost, must use HTTPS
|
143
|
-
unless [ "localhost", "127.0.0.1" ].include?(parsed.host) || parsed.scheme == "https"
|
144
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must use HTTPS"
|
145
|
-
end
|
146
|
-
rescue URI::InvalidURIError
|
147
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Invalid redirect URI format"
|
148
|
-
end
|
149
|
-
|
150
|
-
def generate_client_id
|
151
|
-
# Generate a unique client identifier
|
152
|
-
"mcp_#{SecureRandom.hex(16)}"
|
153
|
-
end
|
154
|
-
|
155
|
-
def generate_client_secret
|
156
|
-
# Generate a secure client secret
|
157
|
-
SecureRandom.urlsafe_base64(32)
|
158
|
-
end
|
159
|
-
|
160
|
-
def generate_registration_access_token(_client_id)
|
161
|
-
# Generate a token for managing this registration
|
162
|
-
SecureRandom.urlsafe_base64(32)
|
163
|
-
end
|
164
|
-
|
165
|
-
def registration_client_url(client_id)
|
166
|
-
"#{request.base_url}/oauth/register/#{client_id}"
|
167
|
-
end
|
168
|
-
|
169
|
-
def supported_grant_types
|
170
|
-
grants = [ "authorization_code" ]
|
171
|
-
grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
|
172
|
-
grants << "client_credentials" if oauth_config[:enable_client_credentials]
|
173
|
-
grants
|
174
|
-
end
|
175
|
-
|
176
|
-
def supported_response_types
|
177
|
-
[ "code" ]
|
178
|
-
end
|
179
|
-
|
180
|
-
def supported_auth_methods
|
181
|
-
methods = %w[client_secret_basic client_secret_post]
|
182
|
-
methods << "none" if oauth_config.fetch(:allow_public_clients, true)
|
183
|
-
methods
|
184
|
-
end
|
185
|
-
|
186
|
-
def render_registration_error(error_code, description)
|
187
|
-
render json: {
|
188
|
-
error: error_code,
|
189
|
-
error_description: description
|
190
|
-
}, status: :bad_request
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
# Custom error for invalid client metadata
|
195
|
-
class InvalidClientMetadataError < Error
|
196
|
-
def initialize(message = "Invalid client metadata")
|
197
|
-
super(message, "invalid_client_metadata")
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|