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,234 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "faraday"
|
4
|
-
require "pkce_challenge"
|
5
|
-
require "securerandom"
|
6
|
-
require "uri"
|
7
|
-
require "json"
|
8
|
-
|
9
|
-
module ActionMCP
|
10
|
-
module Client
|
11
|
-
# OAuth client provider for MCP client authentication
|
12
|
-
# Implements OAuth 2.1 authorization code flow with PKCE
|
13
|
-
class OauthClientProvider
|
14
|
-
class AuthenticationError < StandardError; end
|
15
|
-
class TokenExpiredError < StandardError; end
|
16
|
-
attr_reader :redirect_url, :client_metadata, :authorization_server_url
|
17
|
-
|
18
|
-
def initialize(
|
19
|
-
authorization_server_url:,
|
20
|
-
redirect_url:,
|
21
|
-
client_metadata: {},
|
22
|
-
storage: nil,
|
23
|
-
logger: ActionMCP.logger
|
24
|
-
)
|
25
|
-
@authorization_server_url = URI(authorization_server_url)
|
26
|
-
@redirect_url = URI(redirect_url)
|
27
|
-
@client_metadata = default_client_metadata.merge(client_metadata)
|
28
|
-
@storage = storage || MemoryStorage.new
|
29
|
-
@logger = logger
|
30
|
-
@http_client = build_http_client
|
31
|
-
end
|
32
|
-
|
33
|
-
# Get current access token for authorization headers
|
34
|
-
def access_token
|
35
|
-
tokens = current_tokens
|
36
|
-
return nil unless tokens
|
37
|
-
|
38
|
-
if token_expired?(tokens)
|
39
|
-
refresh_tokens! if tokens[:refresh_token]
|
40
|
-
tokens = current_tokens
|
41
|
-
end
|
42
|
-
|
43
|
-
tokens&.dig(:access_token)
|
44
|
-
end
|
45
|
-
|
46
|
-
# Check if client has valid authentication
|
47
|
-
def authenticated?
|
48
|
-
!access_token.nil?
|
49
|
-
end
|
50
|
-
|
51
|
-
# Start OAuth authorization flow
|
52
|
-
def start_authorization_flow(scope: nil, state: nil)
|
53
|
-
# Generate PKCE challenge
|
54
|
-
pkce = PkceChallenge.challenge
|
55
|
-
code_verifier = pkce.code_verifier
|
56
|
-
code_challenge = pkce.code_challenge
|
57
|
-
@storage.save_code_verifier(code_verifier)
|
58
|
-
|
59
|
-
# Build authorization URL
|
60
|
-
auth_params = {
|
61
|
-
response_type: "code",
|
62
|
-
client_id: client_id,
|
63
|
-
redirect_uri: @redirect_url.to_s,
|
64
|
-
code_challenge: code_challenge,
|
65
|
-
code_challenge_method: "S256"
|
66
|
-
}
|
67
|
-
auth_params[:scope] = scope if scope
|
68
|
-
auth_params[:state] = state if state
|
69
|
-
|
70
|
-
authorization_url = build_url(server_metadata[:authorization_endpoint], auth_params)
|
71
|
-
|
72
|
-
log_debug("Starting OAuth flow: #{authorization_url}")
|
73
|
-
authorization_url
|
74
|
-
end
|
75
|
-
|
76
|
-
# Complete OAuth flow with authorization code
|
77
|
-
def complete_authorization_flow(authorization_code, state: nil)
|
78
|
-
code_verifier = @storage.load_code_verifier
|
79
|
-
raise AuthenticationError, "No code verifier found" unless code_verifier
|
80
|
-
|
81
|
-
# Exchange code for tokens
|
82
|
-
token_params = {
|
83
|
-
grant_type: "authorization_code",
|
84
|
-
code: authorization_code,
|
85
|
-
redirect_uri: @redirect_url.to_s,
|
86
|
-
code_verifier: code_verifier,
|
87
|
-
client_id: client_id
|
88
|
-
}
|
89
|
-
|
90
|
-
response = @http_client.post(server_metadata[:token_endpoint]) do |req|
|
91
|
-
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
92
|
-
req.headers["Accept"] = "application/json"
|
93
|
-
req.body = URI.encode_www_form(token_params)
|
94
|
-
end
|
95
|
-
|
96
|
-
handle_token_response(response)
|
97
|
-
end
|
98
|
-
|
99
|
-
# Refresh access token using refresh token
|
100
|
-
def refresh_tokens!
|
101
|
-
tokens = current_tokens
|
102
|
-
refresh_token = tokens&.dig(:refresh_token)
|
103
|
-
raise TokenExpiredError, "No refresh token available" unless refresh_token
|
104
|
-
|
105
|
-
token_params = {
|
106
|
-
grant_type: "refresh_token",
|
107
|
-
refresh_token: refresh_token,
|
108
|
-
client_id: client_id
|
109
|
-
}
|
110
|
-
|
111
|
-
response = @http_client.post(server_metadata[:token_endpoint]) do |req|
|
112
|
-
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
113
|
-
req.headers["Accept"] = "application/json"
|
114
|
-
req.body = URI.encode_www_form(token_params)
|
115
|
-
end
|
116
|
-
|
117
|
-
handle_token_response(response)
|
118
|
-
end
|
119
|
-
|
120
|
-
# Clear stored tokens (logout)
|
121
|
-
def clear_tokens!
|
122
|
-
@storage.clear_tokens
|
123
|
-
@storage.clear_code_verifier if @storage.respond_to?(:clear_code_verifier)
|
124
|
-
log_debug("Cleared OAuth tokens and code verifier")
|
125
|
-
end
|
126
|
-
|
127
|
-
# Get client information for registration
|
128
|
-
def client_information
|
129
|
-
@storage.load_client_information
|
130
|
-
end
|
131
|
-
|
132
|
-
# Save client information after registration
|
133
|
-
def save_client_information(client_info)
|
134
|
-
@storage.save_client_information(client_info)
|
135
|
-
end
|
136
|
-
|
137
|
-
# Get authorization headers for HTTP requests
|
138
|
-
def authorization_headers
|
139
|
-
token = access_token
|
140
|
-
return {} unless token
|
141
|
-
|
142
|
-
{ "Authorization" => "Bearer #{token}" }
|
143
|
-
end
|
144
|
-
|
145
|
-
private
|
146
|
-
|
147
|
-
def current_tokens
|
148
|
-
@storage.load_tokens
|
149
|
-
end
|
150
|
-
|
151
|
-
def save_tokens(tokens)
|
152
|
-
@storage.save_tokens(tokens)
|
153
|
-
end
|
154
|
-
|
155
|
-
def token_expired?(tokens)
|
156
|
-
expires_at = tokens[:expires_at]
|
157
|
-
return false unless expires_at
|
158
|
-
|
159
|
-
Time.at(expires_at) <= Time.now + 30 # 30 second buffer
|
160
|
-
end
|
161
|
-
|
162
|
-
def client_id
|
163
|
-
client_info = client_information
|
164
|
-
client_info&.dig(:client_id) || @client_metadata[:client_id]
|
165
|
-
end
|
166
|
-
|
167
|
-
def server_metadata
|
168
|
-
@server_metadata ||= fetch_server_metadata
|
169
|
-
end
|
170
|
-
|
171
|
-
def fetch_server_metadata
|
172
|
-
well_known_url = @authorization_server_url.dup
|
173
|
-
well_known_url.path = "/.well-known/oauth-authorization-server"
|
174
|
-
|
175
|
-
response = @http_client.get(well_known_url)
|
176
|
-
raise AuthenticationError, "Failed to fetch server metadata: #{response.status}" unless response.success?
|
177
|
-
|
178
|
-
JSON.parse(response.body, symbolize_names: true)
|
179
|
-
end
|
180
|
-
|
181
|
-
def handle_token_response(response)
|
182
|
-
unless response.success?
|
183
|
-
error_body = begin
|
184
|
-
JSON.parse(response.body)
|
185
|
-
rescue StandardError
|
186
|
-
{}
|
187
|
-
end
|
188
|
-
error_msg = error_body["error_description"] || error_body["error"] || "Token request failed"
|
189
|
-
raise AuthenticationError, "#{error_msg} (#{response.status})"
|
190
|
-
end
|
191
|
-
|
192
|
-
token_data = JSON.parse(response.body, symbolize_names: true)
|
193
|
-
|
194
|
-
# Calculate token expiration
|
195
|
-
token_data[:expires_at] = Time.now.to_i + token_data[:expires_in].to_i if token_data[:expires_in]
|
196
|
-
|
197
|
-
save_tokens(token_data)
|
198
|
-
log_debug("OAuth tokens obtained successfully")
|
199
|
-
token_data
|
200
|
-
end
|
201
|
-
|
202
|
-
def build_url(base_url, params)
|
203
|
-
uri = URI(base_url)
|
204
|
-
uri.query = URI.encode_www_form(params)
|
205
|
-
uri.to_s
|
206
|
-
end
|
207
|
-
|
208
|
-
def build_http_client
|
209
|
-
Faraday.new do |f|
|
210
|
-
f.headers["User-Agent"] = "ActionMCP-OAuth/#{ActionMCP.gem_version}"
|
211
|
-
f.options.timeout = 30
|
212
|
-
f.options.open_timeout = 10
|
213
|
-
f.adapter :net_http
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def default_client_metadata
|
218
|
-
{
|
219
|
-
client_name: "ActionMCP Client",
|
220
|
-
client_uri: "https://github.com/anthropics/action_mcp",
|
221
|
-
redirect_uris: [ @redirect_url.to_s ],
|
222
|
-
grant_types: %w[authorization_code refresh_token],
|
223
|
-
response_types: [ "code" ],
|
224
|
-
token_endpoint_auth_method: "none", # Public client
|
225
|
-
code_challenge_methods_supported: [ "S256" ]
|
226
|
-
}
|
227
|
-
end
|
228
|
-
|
229
|
-
def log_debug(message)
|
230
|
-
@logger.debug("[ActionMCP::OAuthClientProvider] #{message}")
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "jwt"
|
4
|
-
|
5
|
-
module ActionMCP
|
6
|
-
class JwtDecoder
|
7
|
-
class DecodeError < StandardError; end
|
8
|
-
|
9
|
-
# Configurable defaults
|
10
|
-
class << self
|
11
|
-
attr_accessor :secret, :algorithm
|
12
|
-
|
13
|
-
def decode(token)
|
14
|
-
payload, _header = JWT.decode(token, secret, true, { algorithm: algorithm })
|
15
|
-
payload
|
16
|
-
rescue JWT::ExpiredSignature
|
17
|
-
raise DecodeError, "Token has expired"
|
18
|
-
rescue JWT::DecodeError
|
19
|
-
# Simplify the error message for invalid tokens
|
20
|
-
raise DecodeError, "Invalid token"
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# Defaults (can be overridden in an initializer)
|
25
|
-
self.secret = ENV.fetch("ACTION_MCP_JWT_SECRET", "change-me")
|
26
|
-
self.algorithm = "HS256"
|
27
|
-
end
|
28
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
class JwtIdentifier < GatewayIdentifier
|
5
|
-
identifier :user
|
6
|
-
authenticates :jwt
|
7
|
-
|
8
|
-
def resolve
|
9
|
-
token = extract_bearer_token
|
10
|
-
raise Unauthorized, "Missing JWT" unless token
|
11
|
-
|
12
|
-
payload = ActionMCP::JwtDecoder.decode(token)
|
13
|
-
user = User.find_by(id: payload["sub"] || payload["user_id"])
|
14
|
-
return user if user
|
15
|
-
|
16
|
-
raise Unauthorized, "Invalid JWT user"
|
17
|
-
rescue ActionMCP::JwtDecoder::DecodeError => e
|
18
|
-
raise Unauthorized, "Invalid JWT token: #{e.message}"
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def extract_bearer_token
|
24
|
-
header = @request.env["HTTP_AUTHORIZATION"] || ""
|
25
|
-
header[/\ABearer (.+)\z/, 1]
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
class NoneIdentifier < GatewayIdentifier
|
5
|
-
identifier :user
|
6
|
-
authenticates :none
|
7
|
-
|
8
|
-
def resolve
|
9
|
-
Rails.env.production? &&
|
10
|
-
raise(Unauthorized, "No auth allowed in production")
|
11
|
-
|
12
|
-
return "anonymous_user" unless defined?(User)
|
13
|
-
|
14
|
-
User.find_or_create_by!(email: "dev@localhost") do |user|
|
15
|
-
user.name = "Development User" if user.respond_to?(:name=)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
class OAuthIdentifier < GatewayIdentifier
|
5
|
-
identifier :user
|
6
|
-
authenticates :oauth
|
7
|
-
|
8
|
-
def resolve
|
9
|
-
info = @request.env["action_mcp.oauth_token_info"] or
|
10
|
-
raise Unauthorized, "Missing OAuth info"
|
11
|
-
|
12
|
-
uid = info["user_id"] || info["sub"] || info[:user_id]
|
13
|
-
raise Unauthorized, "Invalid OAuth info" unless uid
|
14
|
-
|
15
|
-
# Try to find existing user or create one for demo purposes
|
16
|
-
user = User.find_by(email: uid) ||
|
17
|
-
User.find_by(email: "#{uid}@example.com") ||
|
18
|
-
create_oauth_user(uid)
|
19
|
-
|
20
|
-
user || raise(Unauthorized, "Unable to resolve OAuth user")
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def create_oauth_user(uid)
|
26
|
-
return nil unless defined?(User)
|
27
|
-
|
28
|
-
email = uid.include?("@") ? uid : "#{uid}@example.com"
|
29
|
-
User.create!(email: email)
|
30
|
-
rescue ActiveRecord::RecordInvalid
|
31
|
-
nil
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,183 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
module OAuth
|
5
|
-
# ActiveRecord storage for OAuth tokens and codes
|
6
|
-
# This is suitable for production multi-server environments
|
7
|
-
class ActiveRecordStorage
|
8
|
-
# Authorization code storage
|
9
|
-
def store_authorization_code(code, data)
|
10
|
-
OAuthToken.create!(
|
11
|
-
token: code,
|
12
|
-
token_type: OAuthToken::AUTHORIZATION_CODE,
|
13
|
-
client_id: data[:client_id],
|
14
|
-
user_id: data[:user_id],
|
15
|
-
redirect_uri: data[:redirect_uri],
|
16
|
-
scope: data[:scope],
|
17
|
-
code_challenge: data[:code_challenge],
|
18
|
-
code_challenge_method: data[:code_challenge_method],
|
19
|
-
expires_at: data[:expires_at],
|
20
|
-
metadata: data.except(:client_id, :user_id, :redirect_uri, :scope,
|
21
|
-
:code_challenge, :code_challenge_method, :expires_at)
|
22
|
-
)
|
23
|
-
end
|
24
|
-
|
25
|
-
def retrieve_authorization_code(code)
|
26
|
-
token = OAuthToken.authorization_codes.active.find_by(token: code)
|
27
|
-
return nil unless token
|
28
|
-
|
29
|
-
{
|
30
|
-
client_id: token.client_id,
|
31
|
-
user_id: token.user_id,
|
32
|
-
redirect_uri: token.redirect_uri,
|
33
|
-
scope: token.scope,
|
34
|
-
code_challenge: token.code_challenge,
|
35
|
-
code_challenge_method: token.code_challenge_method,
|
36
|
-
expires_at: token.expires_at,
|
37
|
-
created_at: token.created_at
|
38
|
-
}.merge(token.metadata || {})
|
39
|
-
end
|
40
|
-
|
41
|
-
def remove_authorization_code(code)
|
42
|
-
OAuthToken.authorization_codes.where(token: code).destroy_all
|
43
|
-
end
|
44
|
-
|
45
|
-
# Access token storage
|
46
|
-
def store_access_token(token, data)
|
47
|
-
OAuthToken.create!(
|
48
|
-
token: token,
|
49
|
-
token_type: OAuthToken::ACCESS_TOKEN,
|
50
|
-
client_id: data[:client_id],
|
51
|
-
user_id: data[:user_id],
|
52
|
-
scope: data[:scope],
|
53
|
-
expires_at: data[:expires_at],
|
54
|
-
metadata: data.except(:client_id, :user_id, :scope, :expires_at)
|
55
|
-
)
|
56
|
-
end
|
57
|
-
|
58
|
-
def retrieve_access_token(token)
|
59
|
-
token_record = OAuthToken.access_tokens.find_by(token: token)
|
60
|
-
return nil unless token_record
|
61
|
-
|
62
|
-
{
|
63
|
-
client_id: token_record.client_id,
|
64
|
-
user_id: token_record.user_id,
|
65
|
-
scope: token_record.scope,
|
66
|
-
expires_at: token_record.expires_at,
|
67
|
-
created_at: token_record.created_at,
|
68
|
-
active: token_record.still_valid?
|
69
|
-
}.merge(token_record.metadata || {})
|
70
|
-
end
|
71
|
-
|
72
|
-
def remove_access_token(token)
|
73
|
-
OAuthToken.access_tokens.where(token: token).destroy_all
|
74
|
-
end
|
75
|
-
|
76
|
-
# Refresh token storage
|
77
|
-
def store_refresh_token(token, data)
|
78
|
-
OAuthToken.create!(
|
79
|
-
token: token,
|
80
|
-
token_type: OAuthToken::REFRESH_TOKEN,
|
81
|
-
client_id: data[:client_id],
|
82
|
-
user_id: data[:user_id],
|
83
|
-
scope: data[:scope],
|
84
|
-
access_token: data[:access_token],
|
85
|
-
expires_at: data[:expires_at],
|
86
|
-
metadata: data.except(:client_id, :user_id, :scope, :access_token, :expires_at)
|
87
|
-
)
|
88
|
-
end
|
89
|
-
|
90
|
-
def retrieve_refresh_token(token)
|
91
|
-
token_record = OAuthToken.refresh_tokens.active.find_by(token: token)
|
92
|
-
return nil unless token_record
|
93
|
-
|
94
|
-
{
|
95
|
-
client_id: token_record.client_id,
|
96
|
-
user_id: token_record.user_id,
|
97
|
-
scope: token_record.scope,
|
98
|
-
access_token: token_record.access_token,
|
99
|
-
expires_at: token_record.expires_at,
|
100
|
-
created_at: token_record.created_at
|
101
|
-
}.merge(token_record.metadata || {})
|
102
|
-
end
|
103
|
-
|
104
|
-
def update_refresh_token(token, new_access_token)
|
105
|
-
token_record = OAuthToken.refresh_tokens.find_by(token: token)
|
106
|
-
token_record&.update!(access_token: new_access_token)
|
107
|
-
end
|
108
|
-
|
109
|
-
def remove_refresh_token(token)
|
110
|
-
OAuthToken.refresh_tokens.where(token: token).destroy_all
|
111
|
-
end
|
112
|
-
|
113
|
-
# Client registration storage
|
114
|
-
def store_client_registration(client_id, data)
|
115
|
-
client = OAuthClient.new
|
116
|
-
|
117
|
-
# Map data fields to model attributes
|
118
|
-
client.client_id = client_id
|
119
|
-
client.client_secret = data[:client_secret]
|
120
|
-
client.client_id_issued_at = data[:client_id_issued_at]
|
121
|
-
client.registration_access_token = data[:registration_access_token]
|
122
|
-
|
123
|
-
# Handle client metadata
|
124
|
-
metadata = data[:client_metadata] || {}
|
125
|
-
%w[
|
126
|
-
client_name redirect_uris grant_types response_types
|
127
|
-
token_endpoint_auth_method scope
|
128
|
-
].each do |field|
|
129
|
-
client.send("#{field}=", metadata[field]) if metadata.key?(field)
|
130
|
-
end
|
131
|
-
|
132
|
-
# Store any additional metadata
|
133
|
-
known_fields = %w[
|
134
|
-
client_name redirect_uris grant_types response_types
|
135
|
-
token_endpoint_auth_method scope
|
136
|
-
]
|
137
|
-
additional_metadata = metadata.except(*known_fields)
|
138
|
-
client.metadata = additional_metadata if additional_metadata.present?
|
139
|
-
|
140
|
-
client.save!
|
141
|
-
data
|
142
|
-
end
|
143
|
-
|
144
|
-
def retrieve_client_registration(client_id)
|
145
|
-
client = OAuthClient.active.find_by(client_id: client_id)
|
146
|
-
return nil unless client
|
147
|
-
|
148
|
-
{
|
149
|
-
client_id: client.client_id,
|
150
|
-
client_secret: client.client_secret,
|
151
|
-
client_id_issued_at: client.client_id_issued_at,
|
152
|
-
registration_access_token: client.registration_access_token,
|
153
|
-
client_metadata: client.to_api_response
|
154
|
-
}
|
155
|
-
end
|
156
|
-
|
157
|
-
def remove_client_registration(client_id)
|
158
|
-
OAuthClient.where(client_id: client_id).destroy_all
|
159
|
-
end
|
160
|
-
|
161
|
-
# Cleanup expired tokens
|
162
|
-
def cleanup_expired
|
163
|
-
OAuthToken.cleanup_expired
|
164
|
-
end
|
165
|
-
|
166
|
-
# Statistics (for debugging/monitoring)
|
167
|
-
def stats
|
168
|
-
{
|
169
|
-
authorization_codes: OAuthToken.authorization_codes.active.count,
|
170
|
-
access_tokens: OAuthToken.access_tokens.active.count,
|
171
|
-
refresh_tokens: OAuthToken.refresh_tokens.active.count,
|
172
|
-
client_registrations: OAuthClient.active.count
|
173
|
-
}
|
174
|
-
end
|
175
|
-
|
176
|
-
# Clear all data (for testing)
|
177
|
-
def clear_all
|
178
|
-
OAuthToken.delete_all
|
179
|
-
OAuthClient.delete_all
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
module OAuth
|
5
|
-
# Base OAuth error class
|
6
|
-
class Error < StandardError
|
7
|
-
attr_reader :oauth_error_code
|
8
|
-
|
9
|
-
def initialize(message, oauth_error_code = "invalid_request")
|
10
|
-
super(message)
|
11
|
-
@oauth_error_code = oauth_error_code
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
# OAuth 2.1 standard error types
|
16
|
-
class InvalidRequestError < Error
|
17
|
-
def initialize(message = "Invalid request")
|
18
|
-
super(message, "invalid_request")
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class InvalidClientError < Error
|
23
|
-
def initialize(message = "Invalid client")
|
24
|
-
super(message, "invalid_client")
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class InvalidGrantError < Error
|
29
|
-
def initialize(message = "Invalid grant")
|
30
|
-
super(message, "invalid_grant")
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class UnauthorizedClientError < Error
|
35
|
-
def initialize(message = "Unauthorized client")
|
36
|
-
super(message, "unauthorized_client")
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class UnsupportedGrantTypeError < Error
|
41
|
-
def initialize(message = "Unsupported grant type")
|
42
|
-
super(message, "unsupported_grant_type")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class InvalidScopeError < Error
|
47
|
-
def initialize(message = "Invalid scope")
|
48
|
-
super(message, "invalid_scope")
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class InvalidTokenError < Error
|
53
|
-
def initialize(message = "Invalid token")
|
54
|
-
super(message, "invalid_token")
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
class InsufficientScopeError < Error
|
59
|
-
attr_reader :required_scope
|
60
|
-
|
61
|
-
def initialize(message = "Insufficient scope", required_scope = nil)
|
62
|
-
super(message, "insufficient_scope")
|
63
|
-
@required_scope = required_scope
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
class ServerError < Error
|
68
|
-
def initialize(message = "Server error")
|
69
|
-
super(message, "server_error")
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
class TemporarilyUnavailableError < Error
|
74
|
-
def initialize(message = "Temporarily unavailable")
|
75
|
-
super(message, "temporarily_unavailable")
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|