actionmcp 0.72.0 → 0.80.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 +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/app/models/concerns/{mcp_console_helpers.rb → action_mcp/mcp_console_helpers.rb} +4 -3
- data/app/models/concerns/{mcp_message_inspect.rb → action_mcp/mcp_message_inspect.rb} +4 -3
- 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/resource_template.rb +1 -0
- data/lib/action_mcp/server/base_session.rb +2 -0
- data/lib/action_mcp/server/resources.rb +8 -7
- 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 +15 -99
- 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,135 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "base64"
|
5
|
-
|
6
|
-
module ActionMCP
|
7
|
-
module Client
|
8
|
-
# JWT client provider for MCP client authentication
|
9
|
-
# Provides clean JWT token management for ActionMCP client connections
|
10
|
-
class JwtClientProvider
|
11
|
-
class AuthenticationError < StandardError; end
|
12
|
-
class TokenExpiredError < StandardError; end
|
13
|
-
|
14
|
-
attr_reader :storage
|
15
|
-
|
16
|
-
def initialize(token: nil, storage: nil, logger: ActionMCP.logger)
|
17
|
-
@storage = storage || MemoryStorage.new
|
18
|
-
@logger = logger
|
19
|
-
|
20
|
-
# If token provided during initialization, store it
|
21
|
-
return unless token
|
22
|
-
|
23
|
-
save_token(token)
|
24
|
-
end
|
25
|
-
|
26
|
-
# Check if client has valid authentication
|
27
|
-
def authenticated?
|
28
|
-
token = current_token
|
29
|
-
return false unless token
|
30
|
-
|
31
|
-
!token_expired?(token)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Get authorization headers for HTTP requests
|
35
|
-
def authorization_headers
|
36
|
-
token = current_token
|
37
|
-
return {} unless token
|
38
|
-
|
39
|
-
if token_expired?(token)
|
40
|
-
log_debug("JWT token expired")
|
41
|
-
clear_tokens!
|
42
|
-
return {}
|
43
|
-
end
|
44
|
-
|
45
|
-
{ "Authorization" => "Bearer #{token}" }
|
46
|
-
end
|
47
|
-
|
48
|
-
# Set/update the JWT token
|
49
|
-
def set_token(token)
|
50
|
-
save_token(token)
|
51
|
-
log_debug("JWT token updated")
|
52
|
-
end
|
53
|
-
|
54
|
-
# Clear stored tokens (logout)
|
55
|
-
def clear_tokens!
|
56
|
-
@storage.clear_token
|
57
|
-
log_debug("Cleared JWT token")
|
58
|
-
end
|
59
|
-
|
60
|
-
# Get current valid token
|
61
|
-
def access_token
|
62
|
-
token = current_token
|
63
|
-
return nil unless token
|
64
|
-
return nil if token_expired?(token)
|
65
|
-
|
66
|
-
token
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def current_token
|
72
|
-
@storage.load_token
|
73
|
-
end
|
74
|
-
|
75
|
-
def save_token(token)
|
76
|
-
@storage.save_token(token)
|
77
|
-
end
|
78
|
-
|
79
|
-
def token_expired?(token)
|
80
|
-
return false unless token
|
81
|
-
|
82
|
-
begin
|
83
|
-
payload = decode_jwt_payload(token)
|
84
|
-
exp = payload["exp"]
|
85
|
-
return false unless exp
|
86
|
-
|
87
|
-
# Add 30 second buffer for clock skew
|
88
|
-
Time.at(exp) <= Time.now + 30
|
89
|
-
rescue StandardError => e
|
90
|
-
log_debug("Error checking token expiration: #{e.message}")
|
91
|
-
true # Treat invalid tokens as expired
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def decode_jwt_payload(token)
|
96
|
-
# Split JWT into parts
|
97
|
-
parts = token.split(".")
|
98
|
-
raise AuthenticationError, "Invalid JWT format" unless parts.length == 3
|
99
|
-
|
100
|
-
# Decode payload (second part)
|
101
|
-
payload_base64 = parts[1]
|
102
|
-
# Add padding if needed
|
103
|
-
payload_base64 += "=" * (4 - payload_base64.length % 4) if payload_base64.length % 4 != 0
|
104
|
-
|
105
|
-
payload_json = Base64.urlsafe_decode64(payload_base64)
|
106
|
-
JSON.parse(payload_json)
|
107
|
-
rescue StandardError => e
|
108
|
-
raise AuthenticationError, "Failed to decode JWT: #{e.message}"
|
109
|
-
end
|
110
|
-
|
111
|
-
def log_debug(message)
|
112
|
-
@logger.debug("[ActionMCP::JwtClientProvider] #{message}")
|
113
|
-
end
|
114
|
-
|
115
|
-
# Simple memory storage for JWT tokens
|
116
|
-
class MemoryStorage
|
117
|
-
def initialize
|
118
|
-
@token = nil
|
119
|
-
end
|
120
|
-
|
121
|
-
def save_token(token)
|
122
|
-
@token = token
|
123
|
-
end
|
124
|
-
|
125
|
-
def load_token
|
126
|
-
@token
|
127
|
-
end
|
128
|
-
|
129
|
-
def clear_token
|
130
|
-
@token = nil
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionMCP
|
4
|
-
module Client
|
5
|
-
class OauthClientProvider
|
6
|
-
# Simple in-memory storage for development
|
7
|
-
# In production, use persistent storage
|
8
|
-
class MemoryStorage
|
9
|
-
def initialize
|
10
|
-
@data = {}
|
11
|
-
end
|
12
|
-
|
13
|
-
def save_tokens(tokens)
|
14
|
-
@data[:tokens] = tokens
|
15
|
-
end
|
16
|
-
|
17
|
-
def load_tokens
|
18
|
-
@data[:tokens]
|
19
|
-
end
|
20
|
-
|
21
|
-
def clear_tokens
|
22
|
-
@data.delete(:tokens)
|
23
|
-
end
|
24
|
-
|
25
|
-
def save_code_verifier(verifier)
|
26
|
-
@data[:code_verifier] = verifier
|
27
|
-
end
|
28
|
-
|
29
|
-
def load_code_verifier
|
30
|
-
@data[:code_verifier]
|
31
|
-
end
|
32
|
-
|
33
|
-
def clear_code_verifier
|
34
|
-
@data.delete(:code_verifier)
|
35
|
-
end
|
36
|
-
|
37
|
-
def save_client_information(info)
|
38
|
-
@data[:client_information] = info
|
39
|
-
end
|
40
|
-
|
41
|
-
def load_client_information
|
42
|
-
@data[:client_information]
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -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
|