actionmcp 0.71.1 → 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 +187 -16
- data/app/controllers/action_mcp/application_controller.rb +64 -49
- 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 +71 -113
- data/config/routes.rb +0 -11
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +9 -11
- data/lib/action_mcp/client/elicitation.rb +4 -4
- data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
- data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
- data/lib/action_mcp/client.rb +6 -26
- data/lib/action_mcp/configuration.rb +65 -63
- data/lib/action_mcp/engine.rb +1 -10
- data/lib/action_mcp/filtered_logger.rb +3 -7
- data/lib/action_mcp/gateway.rb +7 -11
- 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/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/prompt.rb +2 -0
- data/lib/action_mcp/renderable.rb +1 -1
- data/lib/action_mcp/resource_template.rb +6 -2
- data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
- data/lib/action_mcp/server/base_session_store.rb +86 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +3 -9
- data/lib/action_mcp/server/error_handling.rb +14 -1
- data/lib/action_mcp/server/handlers/router.rb +31 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
- data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
- data/lib/action_mcp/server/prompts.rb +4 -4
- data/lib/action_mcp/server/resources.rb +23 -4
- data/lib/action_mcp/server/session_store_factory.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
- data/lib/action_mcp/server/tools.rb +62 -43
- data/lib/action_mcp/server/transport_handler.rb +2 -4
- data/lib/action_mcp/server/volatile_session_store.rb +1 -93
- data/lib/action_mcp/tagged_stream_logging.rb +2 -2
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
- data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
- data/lib/action_mcp/tool.rb +48 -37
- data/lib/action_mcp/types/float_array_type.rb +5 -3
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -7
- 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 +86 -36
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +18 -100
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
- data/app/models/action_mcp/oauth_client.rb +0 -157
- data/app/models/action_mcp/oauth_token.rb +0 -141
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
- 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 -26
- 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 -134
- data/lib/action_mcp/oauth/middleware.rb +0 -133
- data/lib/action_mcp/oauth/provider.rb +0 -426
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
- data/lib/action_mcp/server/notifications.rb +0 -58
@@ -1,129 +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
|
-
if oauth_config[:enable_dynamic_registration]
|
29
|
-
metadata[:registration_endpoint] = registration_endpoint
|
30
|
-
end
|
31
|
-
|
32
|
-
if oauth_config[:jwks_uri]
|
33
|
-
metadata[:jwks_uri] = oauth_config[:jwks_uri]
|
34
|
-
end
|
35
|
-
|
36
|
-
render json: metadata
|
37
|
-
end
|
38
|
-
|
39
|
-
# GET /.well-known/oauth-protected-resource
|
40
|
-
# Returns Protected Resource Metadata as per RFC 8705
|
41
|
-
def protected_resource
|
42
|
-
metadata = {
|
43
|
-
resource: issuer_url,
|
44
|
-
authorization_servers: [ issuer_url ],
|
45
|
-
scopes_supported: scopes_supported,
|
46
|
-
bearer_methods_supported: [ "header" ],
|
47
|
-
resource_documentation: resource_documentation
|
48
|
-
}
|
49
|
-
|
50
|
-
render json: metadata
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def check_oauth_enabled
|
56
|
-
auth_methods = ActionMCP.configuration.authentication_methods
|
57
|
-
unless auth_methods&.include?("oauth")
|
58
|
-
head :not_found
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def oauth_config
|
63
|
-
@oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
|
64
|
-
end
|
65
|
-
|
66
|
-
def issuer_url
|
67
|
-
@issuer_url ||= oauth_config.fetch(:issuer_url, request.base_url)
|
68
|
-
end
|
69
|
-
|
70
|
-
def authorization_endpoint
|
71
|
-
"#{issuer_url}/oauth/authorize"
|
72
|
-
end
|
73
|
-
|
74
|
-
def token_endpoint
|
75
|
-
"#{issuer_url}/oauth/token"
|
76
|
-
end
|
77
|
-
|
78
|
-
def introspection_endpoint
|
79
|
-
"#{issuer_url}/oauth/introspect"
|
80
|
-
end
|
81
|
-
|
82
|
-
def revocation_endpoint
|
83
|
-
"#{issuer_url}/oauth/revoke"
|
84
|
-
end
|
85
|
-
|
86
|
-
def registration_endpoint
|
87
|
-
"#{issuer_url}/oauth/register"
|
88
|
-
end
|
89
|
-
|
90
|
-
def response_types_supported
|
91
|
-
[ "code" ]
|
92
|
-
end
|
93
|
-
|
94
|
-
def grant_types_supported
|
95
|
-
grants = [ "authorization_code" ]
|
96
|
-
grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
|
97
|
-
grants << "client_credentials" if oauth_config[:enable_client_credentials]
|
98
|
-
grants
|
99
|
-
end
|
100
|
-
|
101
|
-
def token_endpoint_auth_methods_supported
|
102
|
-
methods = [ "client_secret_basic", "client_secret_post" ]
|
103
|
-
methods << "none" if oauth_config[:allow_public_clients]
|
104
|
-
methods
|
105
|
-
end
|
106
|
-
|
107
|
-
def scopes_supported
|
108
|
-
oauth_config.fetch(:scopes_supported, [ "mcp:tools", "mcp:resources", "mcp:prompts" ])
|
109
|
-
end
|
110
|
-
|
111
|
-
def code_challenge_methods_supported
|
112
|
-
methods = []
|
113
|
-
if oauth_config[:pkce_required] || oauth_config[:pkce_supported]
|
114
|
-
methods << "S256"
|
115
|
-
methods << "plain" if oauth_config[:allow_plain_pkce]
|
116
|
-
end
|
117
|
-
methods
|
118
|
-
end
|
119
|
-
|
120
|
-
def service_documentation
|
121
|
-
oauth_config.fetch(:service_documentation, "#{request.base_url}/docs")
|
122
|
-
end
|
123
|
-
|
124
|
-
def resource_documentation
|
125
|
-
oauth_config.fetch(:resource_documentation, "#{request.base_url}/docs/api")
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
@@ -1,206 +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
|
-
if client_metadata["token_endpoint_auth_method"] != "none"
|
27
|
-
client_secret = generate_client_secret
|
28
|
-
end
|
29
|
-
|
30
|
-
# Store client registration
|
31
|
-
client_info = {
|
32
|
-
client_id: client_id,
|
33
|
-
client_secret: client_secret,
|
34
|
-
client_id_issued_at: Time.current.to_i,
|
35
|
-
client_metadata: client_metadata,
|
36
|
-
created_at: Time.current
|
37
|
-
}
|
38
|
-
|
39
|
-
# Save client registration (delegated to provider)
|
40
|
-
ActionMCP::OAuth::Provider.register_client(client_info)
|
41
|
-
|
42
|
-
# Build response according to RFC 7591
|
43
|
-
response_data = {
|
44
|
-
client_id: client_id,
|
45
|
-
client_id_issued_at: client_info[:client_id_issued_at]
|
46
|
-
}
|
47
|
-
|
48
|
-
# Include client secret for confidential clients
|
49
|
-
if client_secret
|
50
|
-
response_data[:client_secret] = client_secret
|
51
|
-
response_data[:client_secret_expires_at] = 0 # Never expires
|
52
|
-
end
|
53
|
-
|
54
|
-
# Include all client metadata in response
|
55
|
-
response_data.merge!(client_metadata)
|
56
|
-
|
57
|
-
# Add registration management fields if enabled
|
58
|
-
if oauth_config[:enable_registration_management]
|
59
|
-
response_data[:registration_access_token] = generate_registration_access_token(client_id)
|
60
|
-
response_data[:registration_client_uri] = registration_client_url(client_id)
|
61
|
-
end
|
62
|
-
|
63
|
-
render json: response_data, status: :created
|
64
|
-
rescue ActionMCP::OAuth::Error => e
|
65
|
-
render_registration_error(e.oauth_error_code, e.message)
|
66
|
-
rescue StandardError => e
|
67
|
-
Rails.logger.error "Registration error: #{e.message}"
|
68
|
-
render_registration_error("invalid_client_metadata", "Invalid client metadata")
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
def check_oauth_enabled
|
74
|
-
auth_methods = ActionMCP.configuration.authentication_methods
|
75
|
-
unless auth_methods&.include?("oauth")
|
76
|
-
head :not_found
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def check_registration_enabled
|
81
|
-
unless oauth_config[:enable_dynamic_registration]
|
82
|
-
head :not_found
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def oauth_config
|
87
|
-
@oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
|
88
|
-
end
|
89
|
-
|
90
|
-
def parse_client_metadata
|
91
|
-
# RFC 7591 requires JSON request body
|
92
|
-
unless request.content_type&.include?("application/json")
|
93
|
-
raise ActionMCP::OAuth::InvalidRequestError, "Content-Type must be application/json"
|
94
|
-
end
|
95
|
-
|
96
|
-
JSON.parse(request.body.read)
|
97
|
-
rescue JSON::ParserError
|
98
|
-
raise ActionMCP::OAuth::InvalidRequestError, "Invalid JSON"
|
99
|
-
end
|
100
|
-
|
101
|
-
def validate_client_metadata(metadata)
|
102
|
-
# Validate redirect URIs (required for authorization code flow)
|
103
|
-
if metadata["grant_types"]&.include?("authorization_code") ||
|
104
|
-
metadata["response_types"]&.include?("code")
|
105
|
-
unless metadata["redirect_uris"].is_a?(Array) && metadata["redirect_uris"].any?
|
106
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "redirect_uris required for authorization code flow"
|
107
|
-
end
|
108
|
-
|
109
|
-
# Validate redirect URI format
|
110
|
-
metadata["redirect_uris"].each do |uri|
|
111
|
-
validate_redirect_uri(uri)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
# Validate grant types
|
116
|
-
if metadata["grant_types"]
|
117
|
-
unsupported = metadata["grant_types"] - supported_grant_types
|
118
|
-
if unsupported.any?
|
119
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported grant types: #{unsupported.join(', ')}"
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
# Validate response types
|
124
|
-
if metadata["response_types"]
|
125
|
-
unsupported = metadata["response_types"] - supported_response_types
|
126
|
-
if unsupported.any?
|
127
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported response types: #{unsupported.join(', ')}"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Validate token endpoint auth method
|
132
|
-
if metadata["token_endpoint_auth_method"]
|
133
|
-
unless supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
|
134
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def validate_redirect_uri(uri)
|
140
|
-
parsed = URI.parse(uri)
|
141
|
-
|
142
|
-
# Must be absolute URI
|
143
|
-
unless parsed.absolute?
|
144
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute"
|
145
|
-
end
|
146
|
-
|
147
|
-
# For non-localhost, must use HTTPS
|
148
|
-
unless parsed.host == "localhost" || parsed.host == "127.0.0.1" || parsed.scheme == "https"
|
149
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must use HTTPS"
|
150
|
-
end
|
151
|
-
rescue URI::InvalidURIError
|
152
|
-
raise ActionMCP::OAuth::InvalidClientMetadataError, "Invalid redirect URI format"
|
153
|
-
end
|
154
|
-
|
155
|
-
def generate_client_id
|
156
|
-
# Generate a unique client identifier
|
157
|
-
"mcp_#{SecureRandom.hex(16)}"
|
158
|
-
end
|
159
|
-
|
160
|
-
def generate_client_secret
|
161
|
-
# Generate a secure client secret
|
162
|
-
SecureRandom.urlsafe_base64(32)
|
163
|
-
end
|
164
|
-
|
165
|
-
def generate_registration_access_token(client_id)
|
166
|
-
# Generate a token for managing this registration
|
167
|
-
SecureRandom.urlsafe_base64(32)
|
168
|
-
end
|
169
|
-
|
170
|
-
def registration_client_url(client_id)
|
171
|
-
"#{request.base_url}/oauth/register/#{client_id}"
|
172
|
-
end
|
173
|
-
|
174
|
-
def supported_grant_types
|
175
|
-
grants = [ "authorization_code" ]
|
176
|
-
grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
|
177
|
-
grants << "client_credentials" if oauth_config[:enable_client_credentials]
|
178
|
-
grants
|
179
|
-
end
|
180
|
-
|
181
|
-
def supported_response_types
|
182
|
-
[ "code" ]
|
183
|
-
end
|
184
|
-
|
185
|
-
def supported_auth_methods
|
186
|
-
methods = [ "client_secret_basic", "client_secret_post" ]
|
187
|
-
methods << "none" if oauth_config.fetch(:allow_public_clients, true)
|
188
|
-
methods
|
189
|
-
end
|
190
|
-
|
191
|
-
def render_registration_error(error_code, description)
|
192
|
-
render json: {
|
193
|
-
error: error_code,
|
194
|
-
error_description: description
|
195
|
-
}, status: :bad_request
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
# Custom error for invalid client metadata
|
200
|
-
class InvalidClientMetadataError < Error
|
201
|
-
def initialize(message = "Invalid client metadata")
|
202
|
-
super(message, "invalid_client_metadata")
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
end
|
@@ -1,157 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
# OAuth 2.0 Client model for storing registered clients
|
5
|
-
# == Schema Information
|
6
|
-
#
|
7
|
-
# Table name: action_mcp_oauth_clients
|
8
|
-
#
|
9
|
-
# id :integer not null, primary key
|
10
|
-
# active :boolean default(TRUE)
|
11
|
-
# client_id_issued_at :integer
|
12
|
-
# client_name :string
|
13
|
-
# client_secret :string
|
14
|
-
# client_secret_expires_at :integer
|
15
|
-
# grant_types :json
|
16
|
-
# metadata :json
|
17
|
-
# redirect_uris :json
|
18
|
-
# registration_access_token :string
|
19
|
-
# response_types :json
|
20
|
-
# scope :text
|
21
|
-
# token_endpoint_auth_method :string default("client_secret_basic")
|
22
|
-
# created_at :datetime not null
|
23
|
-
# updated_at :datetime not null
|
24
|
-
# client_id :string not null
|
25
|
-
#
|
26
|
-
# Indexes
|
27
|
-
#
|
28
|
-
# index_action_mcp_oauth_clients_on_active (active)
|
29
|
-
# index_action_mcp_oauth_clients_on_client_id (client_id) UNIQUE
|
30
|
-
# index_action_mcp_oauth_clients_on_client_id_issued_at (client_id_issued_at)
|
31
|
-
#
|
32
|
-
# Implements RFC 7591 Dynamic Client Registration
|
33
|
-
class OAuthClient < ApplicationRecord
|
34
|
-
self.table_name = "action_mcp_oauth_clients"
|
35
|
-
|
36
|
-
# Validations
|
37
|
-
validates :client_id, presence: true, uniqueness: true
|
38
|
-
validates :token_endpoint_auth_method, inclusion: {
|
39
|
-
in: %w[none client_secret_basic client_secret_post client_secret_jwt private_key_jwt]
|
40
|
-
}
|
41
|
-
|
42
|
-
# Scopes
|
43
|
-
scope :active, -> { where(active: true) }
|
44
|
-
scope :expired, -> { where("client_secret_expires_at < ?", Time.current.to_i).where.not(client_secret_expires_at: [ nil, 0 ]) }
|
45
|
-
|
46
|
-
# Callbacks
|
47
|
-
before_create :set_issued_at
|
48
|
-
|
49
|
-
# Check if client secret is expired
|
50
|
-
def secret_expired?
|
51
|
-
return false if client_secret_expires_at.nil? || client_secret_expires_at == 0
|
52
|
-
Time.current.to_i > client_secret_expires_at
|
53
|
-
end
|
54
|
-
|
55
|
-
# Check if client is public (no authentication required)
|
56
|
-
def public_client?
|
57
|
-
token_endpoint_auth_method == "none"
|
58
|
-
end
|
59
|
-
|
60
|
-
# Check if client is confidential (authentication required)
|
61
|
-
def confidential_client?
|
62
|
-
!public_client?
|
63
|
-
end
|
64
|
-
|
65
|
-
# Validate redirect URI against registered URIs
|
66
|
-
def valid_redirect_uri?(uri)
|
67
|
-
return false if redirect_uris.blank?
|
68
|
-
redirect_uris.include?(uri)
|
69
|
-
end
|
70
|
-
|
71
|
-
# Check if grant type is supported by this client
|
72
|
-
def supports_grant_type?(grant_type)
|
73
|
-
grant_types.include?(grant_type)
|
74
|
-
end
|
75
|
-
|
76
|
-
# Check if response type is supported by this client
|
77
|
-
def supports_response_type?(response_type)
|
78
|
-
response_types.include?(response_type)
|
79
|
-
end
|
80
|
-
|
81
|
-
# Check if scope is allowed for this client
|
82
|
-
def valid_scope?(requested_scope)
|
83
|
-
return true if scope.blank? # No scope restrictions
|
84
|
-
|
85
|
-
requested_scopes = requested_scope.split(" ")
|
86
|
-
allowed_scopes = scope.split(" ")
|
87
|
-
|
88
|
-
# All requested scopes must be in allowed scopes
|
89
|
-
(requested_scopes - allowed_scopes).empty?
|
90
|
-
end
|
91
|
-
|
92
|
-
# Convert to hash for API responses
|
93
|
-
def to_api_response
|
94
|
-
response = {
|
95
|
-
client_id: client_id,
|
96
|
-
client_id_issued_at: client_id_issued_at
|
97
|
-
}
|
98
|
-
|
99
|
-
# Include client secret for confidential clients
|
100
|
-
if client_secret.present?
|
101
|
-
response[:client_secret] = client_secret
|
102
|
-
response[:client_secret_expires_at] = client_secret_expires_at || 0
|
103
|
-
end
|
104
|
-
|
105
|
-
# Include metadata fields
|
106
|
-
%w[
|
107
|
-
client_name redirect_uris grant_types response_types
|
108
|
-
token_endpoint_auth_method scope
|
109
|
-
].each do |field|
|
110
|
-
value = send(field)
|
111
|
-
response[field.to_sym] = value if value.present?
|
112
|
-
end
|
113
|
-
|
114
|
-
# Include additional metadata
|
115
|
-
response.merge!(metadata) if metadata.present?
|
116
|
-
|
117
|
-
response
|
118
|
-
end
|
119
|
-
|
120
|
-
# Create from registration request
|
121
|
-
def self.create_from_registration(client_metadata)
|
122
|
-
client = new
|
123
|
-
|
124
|
-
# Set basic fields
|
125
|
-
client.client_id = "mcp_#{SecureRandom.hex(16)}"
|
126
|
-
|
127
|
-
# Set metadata fields
|
128
|
-
%w[
|
129
|
-
client_name redirect_uris grant_types response_types
|
130
|
-
token_endpoint_auth_method scope
|
131
|
-
].each do |field|
|
132
|
-
client.send("#{field}=", client_metadata[field]) if client_metadata[field]
|
133
|
-
end
|
134
|
-
|
135
|
-
# Generate client secret for confidential clients
|
136
|
-
if client.confidential_client?
|
137
|
-
client.client_secret = SecureRandom.urlsafe_base64(32)
|
138
|
-
end
|
139
|
-
|
140
|
-
# Store any additional metadata
|
141
|
-
known_fields = %w[
|
142
|
-
client_name redirect_uris grant_types response_types
|
143
|
-
token_endpoint_auth_method scope
|
144
|
-
]
|
145
|
-
additional_metadata = client_metadata.except(*known_fields)
|
146
|
-
client.metadata = additional_metadata if additional_metadata.present?
|
147
|
-
|
148
|
-
client
|
149
|
-
end
|
150
|
-
|
151
|
-
private
|
152
|
-
|
153
|
-
def set_issued_at
|
154
|
-
self.client_id_issued_at ||= Time.current.to_i
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
@@ -1,141 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
# == Schema Information
|
5
|
-
#
|
6
|
-
# Table name: action_mcp_oauth_tokens
|
7
|
-
#
|
8
|
-
# id :integer not null, primary key
|
9
|
-
# access_token :string
|
10
|
-
# code_challenge :string
|
11
|
-
# code_challenge_method :string
|
12
|
-
# expires_at :datetime
|
13
|
-
# metadata :json
|
14
|
-
# redirect_uri :string
|
15
|
-
# revoked :boolean default(FALSE)
|
16
|
-
# scope :text
|
17
|
-
# token :string not null
|
18
|
-
# token_type :string not null
|
19
|
-
# created_at :datetime not null
|
20
|
-
# updated_at :datetime not null
|
21
|
-
# client_id :string not null
|
22
|
-
# user_id :string
|
23
|
-
#
|
24
|
-
# Indexes
|
25
|
-
#
|
26
|
-
# index_action_mcp_oauth_tokens_on_client_id (client_id)
|
27
|
-
# index_action_mcp_oauth_tokens_on_expires_at (expires_at)
|
28
|
-
# index_action_mcp_oauth_tokens_on_revoked (revoked)
|
29
|
-
# index_action_mcp_oauth_tokens_on_token (token) UNIQUE
|
30
|
-
# index_action_mcp_oauth_tokens_on_token_type (token_type)
|
31
|
-
# index_action_mcp_oauth_tokens_on_token_type_and_expires_at (token_type,expires_at)
|
32
|
-
# index_action_mcp_oauth_tokens_on_user_id (user_id)
|
33
|
-
#
|
34
|
-
# OAuth 2.0 Token model for storing access tokens, refresh tokens, and authorization codes
|
35
|
-
class OAuthToken < ApplicationRecord
|
36
|
-
self.table_name = "action_mcp_oauth_tokens"
|
37
|
-
|
38
|
-
# Token types
|
39
|
-
ACCESS_TOKEN = "access_token"
|
40
|
-
REFRESH_TOKEN = "refresh_token"
|
41
|
-
AUTHORIZATION_CODE = "authorization_code"
|
42
|
-
|
43
|
-
# Validations
|
44
|
-
validates :token, presence: true, uniqueness: true
|
45
|
-
validates :token_type, presence: true, inclusion: { in: [ ACCESS_TOKEN, REFRESH_TOKEN, AUTHORIZATION_CODE ] }
|
46
|
-
validates :client_id, presence: true
|
47
|
-
validates :expires_at, presence: true
|
48
|
-
|
49
|
-
# Scopes
|
50
|
-
scope :active, -> { where(revoked: false).where("expires_at > ?", Time.current) }
|
51
|
-
scope :expired, -> { where("expires_at <= ?", Time.current) }
|
52
|
-
scope :access_tokens, -> { where(token_type: ACCESS_TOKEN) }
|
53
|
-
scope :refresh_tokens, -> { where(token_type: REFRESH_TOKEN) }
|
54
|
-
scope :authorization_codes, -> { where(token_type: AUTHORIZATION_CODE) }
|
55
|
-
|
56
|
-
# Check if token is still valid
|
57
|
-
def still_valid?
|
58
|
-
!revoked? && !expired?
|
59
|
-
end
|
60
|
-
|
61
|
-
# Check if token is expired
|
62
|
-
def expired?
|
63
|
-
expires_at <= Time.current
|
64
|
-
end
|
65
|
-
|
66
|
-
# Revoke the token
|
67
|
-
def revoke!
|
68
|
-
update!(revoked: true)
|
69
|
-
end
|
70
|
-
|
71
|
-
# Convert to introspection response
|
72
|
-
def to_introspection_response
|
73
|
-
if still_valid?
|
74
|
-
{
|
75
|
-
active: true,
|
76
|
-
scope: scope,
|
77
|
-
client_id: client_id,
|
78
|
-
username: user_id,
|
79
|
-
token_type: token_type == ACCESS_TOKEN ? "Bearer" : token_type,
|
80
|
-
exp: expires_at.to_i,
|
81
|
-
iat: created_at.to_i,
|
82
|
-
nbf: created_at.to_i,
|
83
|
-
sub: user_id,
|
84
|
-
aud: client_id,
|
85
|
-
iss: ActionMCP.configuration.oauth_config&.dig("issuer_url")
|
86
|
-
}.compact
|
87
|
-
else
|
88
|
-
{ active: false }
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Create authorization code
|
93
|
-
def self.create_authorization_code(client_id:, user_id:, redirect_uri:, scope:, code_challenge: nil, code_challenge_method: nil)
|
94
|
-
create!(
|
95
|
-
token: SecureRandom.urlsafe_base64(32),
|
96
|
-
token_type: AUTHORIZATION_CODE,
|
97
|
-
client_id: client_id,
|
98
|
-
user_id: user_id,
|
99
|
-
redirect_uri: redirect_uri,
|
100
|
-
scope: scope,
|
101
|
-
code_challenge: code_challenge,
|
102
|
-
code_challenge_method: code_challenge_method,
|
103
|
-
expires_at: 10.minutes.from_now
|
104
|
-
)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Create access token
|
108
|
-
def self.create_access_token(client_id:, user_id:, scope:)
|
109
|
-
expires_in = ActionMCP.configuration.oauth_config&.dig("access_token_expires_in") || 3600
|
110
|
-
|
111
|
-
create!(
|
112
|
-
token: SecureRandom.urlsafe_base64(32),
|
113
|
-
token_type: ACCESS_TOKEN,
|
114
|
-
client_id: client_id,
|
115
|
-
user_id: user_id,
|
116
|
-
scope: scope,
|
117
|
-
expires_at: expires_in.seconds.from_now
|
118
|
-
)
|
119
|
-
end
|
120
|
-
|
121
|
-
# Create refresh token
|
122
|
-
def self.create_refresh_token(client_id:, user_id:, scope:, access_token:)
|
123
|
-
expires_in = ActionMCP.configuration.oauth_config&.dig("refresh_token_expires_in") || 7.days.to_i
|
124
|
-
|
125
|
-
create!(
|
126
|
-
token: SecureRandom.urlsafe_base64(32),
|
127
|
-
token_type: REFRESH_TOKEN,
|
128
|
-
client_id: client_id,
|
129
|
-
user_id: user_id,
|
130
|
-
scope: scope,
|
131
|
-
access_token: access_token,
|
132
|
-
expires_at: expires_in.seconds.from_now
|
133
|
-
)
|
134
|
-
end
|
135
|
-
|
136
|
-
# Clean up expired tokens
|
137
|
-
def self.cleanup_expired
|
138
|
-
expired.delete_all
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class AddOAuthToSessions < ActiveRecord::Migration[8.0]
|
4
|
-
def change
|
5
|
-
# Use json for all databases (PostgreSQL, SQLite3, MySQL) for consistency
|
6
|
-
json_type = :json
|
7
|
-
|
8
|
-
add_column :action_mcp_sessions, :oauth_access_token, :string unless column_exists?(:action_mcp_sessions, :oauth_access_token)
|
9
|
-
add_column :action_mcp_sessions, :oauth_refresh_token, :string unless column_exists?(:action_mcp_sessions, :oauth_refresh_token)
|
10
|
-
add_column :action_mcp_sessions, :oauth_token_expires_at, :datetime unless column_exists?(:action_mcp_sessions, :oauth_token_expires_at)
|
11
|
-
add_column :action_mcp_sessions, :oauth_user_context, json_type unless column_exists?(:action_mcp_sessions, :oauth_user_context)
|
12
|
-
add_column :action_mcp_sessions, :authentication_method, :string, default: "none" unless column_exists?(:action_mcp_sessions, :authentication_method)
|
13
|
-
|
14
|
-
# Add indexes for performance
|
15
|
-
add_index :action_mcp_sessions, :oauth_access_token, unique: true unless index_exists?(:action_mcp_sessions, :oauth_access_token)
|
16
|
-
add_index :action_mcp_sessions, :oauth_token_expires_at unless index_exists?(:action_mcp_sessions, :oauth_token_expires_at)
|
17
|
-
add_index :action_mcp_sessions, :authentication_method unless index_exists?(:action_mcp_sessions, :authentication_method)
|
18
|
-
end
|
19
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
|
2
|
-
def change
|
3
|
-
create_table :action_mcp_oauth_clients do |t|
|
4
|
-
t.string :client_id, null: false, index: { unique: true }
|
5
|
-
t.string :client_secret
|
6
|
-
t.string :client_name
|
7
|
-
|
8
|
-
# Store arrays as JSON for database compatibility
|
9
|
-
if connection.adapter_name.downcase.include?('postgresql')
|
10
|
-
t.text :redirect_uris, array: true, default: []
|
11
|
-
t.text :grant_types, array: true, default: [ "authorization_code" ]
|
12
|
-
t.text :response_types, array: true, default: [ "code" ]
|
13
|
-
else
|
14
|
-
# For SQLite and other databases, use JSON
|
15
|
-
t.json :redirect_uris, default: []
|
16
|
-
t.json :grant_types, default: [ "authorization_code" ]
|
17
|
-
t.json :response_types, default: [ "code" ]
|
18
|
-
end
|
19
|
-
|
20
|
-
t.string :token_endpoint_auth_method, default: "client_secret_basic"
|
21
|
-
t.text :scope
|
22
|
-
t.boolean :active, default: true
|
23
|
-
|
24
|
-
# Registration metadata
|
25
|
-
t.integer :client_id_issued_at
|
26
|
-
t.integer :client_secret_expires_at
|
27
|
-
t.string :registration_access_token # OAuth 2.1 Dynamic Client Registration
|
28
|
-
|
29
|
-
# Additional metadata as JSON for database compatibility
|
30
|
-
if connection.adapter_name.downcase.include?('postgresql')
|
31
|
-
t.jsonb :metadata, default: {}
|
32
|
-
else
|
33
|
-
t.json :metadata, default: {}
|
34
|
-
end
|
35
|
-
|
36
|
-
t.timestamps
|
37
|
-
end
|
38
|
-
|
39
|
-
add_index :action_mcp_oauth_clients, :active
|
40
|
-
add_index :action_mcp_oauth_clients, :client_id_issued_at
|
41
|
-
end
|
42
|
-
end
|