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,159 +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, lambda {
|
45
|
-
where("client_secret_expires_at < ?", Time.current.to_i).where.not(client_secret_expires_at: [ nil, 0 ])
|
46
|
-
}
|
47
|
-
|
48
|
-
# Callbacks
|
49
|
-
before_create :set_issued_at
|
50
|
-
|
51
|
-
# Check if client secret is expired
|
52
|
-
def secret_expired?
|
53
|
-
return false if client_secret_expires_at.nil? || client_secret_expires_at.zero?
|
54
|
-
|
55
|
-
Time.current.to_i > client_secret_expires_at
|
56
|
-
end
|
57
|
-
|
58
|
-
# Check if client is public (no authentication required)
|
59
|
-
def public_client?
|
60
|
-
token_endpoint_auth_method == "none"
|
61
|
-
end
|
62
|
-
|
63
|
-
# Check if client is confidential (authentication required)
|
64
|
-
def confidential_client?
|
65
|
-
!public_client?
|
66
|
-
end
|
67
|
-
|
68
|
-
# Validate redirect URI against registered URIs
|
69
|
-
def valid_redirect_uri?(uri)
|
70
|
-
return false if redirect_uris.blank?
|
71
|
-
|
72
|
-
redirect_uris.include?(uri)
|
73
|
-
end
|
74
|
-
|
75
|
-
# Check if grant type is supported by this client
|
76
|
-
def supports_grant_type?(grant_type)
|
77
|
-
grant_types.include?(grant_type)
|
78
|
-
end
|
79
|
-
|
80
|
-
# Check if response type is supported by this client
|
81
|
-
def supports_response_type?(response_type)
|
82
|
-
response_types.include?(response_type)
|
83
|
-
end
|
84
|
-
|
85
|
-
# Check if scope is allowed for this client
|
86
|
-
def valid_scope?(requested_scope)
|
87
|
-
return true if scope.blank? # No scope restrictions
|
88
|
-
|
89
|
-
requested_scopes = requested_scope.split(" ")
|
90
|
-
allowed_scopes = scope.split(" ")
|
91
|
-
|
92
|
-
# All requested scopes must be in allowed scopes
|
93
|
-
(requested_scopes - allowed_scopes).empty?
|
94
|
-
end
|
95
|
-
|
96
|
-
# Convert to hash for API responses
|
97
|
-
def to_api_response
|
98
|
-
response = {
|
99
|
-
client_id: client_id,
|
100
|
-
client_id_issued_at: client_id_issued_at
|
101
|
-
}
|
102
|
-
|
103
|
-
# Include client secret for confidential clients
|
104
|
-
if client_secret.present?
|
105
|
-
response[:client_secret] = client_secret
|
106
|
-
response[:client_secret_expires_at] = client_secret_expires_at || 0
|
107
|
-
end
|
108
|
-
|
109
|
-
# Include metadata fields
|
110
|
-
%w[
|
111
|
-
client_name redirect_uris grant_types response_types
|
112
|
-
token_endpoint_auth_method scope
|
113
|
-
].each do |field|
|
114
|
-
value = send(field)
|
115
|
-
response[field.to_sym] = value if value.present?
|
116
|
-
end
|
117
|
-
|
118
|
-
# Include additional metadata
|
119
|
-
response.merge!(metadata) if metadata.present?
|
120
|
-
|
121
|
-
response
|
122
|
-
end
|
123
|
-
|
124
|
-
# Create from registration request
|
125
|
-
def self.create_from_registration(client_metadata)
|
126
|
-
client = new
|
127
|
-
|
128
|
-
# Set basic fields
|
129
|
-
client.client_id = "mcp_#{SecureRandom.hex(16)}"
|
130
|
-
|
131
|
-
# Set metadata fields
|
132
|
-
%w[
|
133
|
-
client_name redirect_uris grant_types response_types
|
134
|
-
token_endpoint_auth_method scope
|
135
|
-
].each do |field|
|
136
|
-
client.send("#{field}=", client_metadata[field]) if client_metadata[field]
|
137
|
-
end
|
138
|
-
|
139
|
-
# Generate client secret for confidential clients
|
140
|
-
client.client_secret = SecureRandom.urlsafe_base64(32) if client.confidential_client?
|
141
|
-
|
142
|
-
# Store any additional metadata
|
143
|
-
known_fields = %w[
|
144
|
-
client_name redirect_uris grant_types response_types
|
145
|
-
token_endpoint_auth_method scope
|
146
|
-
]
|
147
|
-
additional_metadata = client_metadata.except(*known_fields)
|
148
|
-
client.metadata = additional_metadata if additional_metadata.present?
|
149
|
-
|
150
|
-
client
|
151
|
-
end
|
152
|
-
|
153
|
-
private
|
154
|
-
|
155
|
-
def set_issued_at
|
156
|
-
self.client_id_issued_at ||= Time.current.to_i
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
@@ -1,142 +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,
|
94
|
-
code_challenge_method: nil)
|
95
|
-
create!(
|
96
|
-
token: SecureRandom.urlsafe_base64(32),
|
97
|
-
token_type: AUTHORIZATION_CODE,
|
98
|
-
client_id: client_id,
|
99
|
-
user_id: user_id,
|
100
|
-
redirect_uri: redirect_uri,
|
101
|
-
scope: scope,
|
102
|
-
code_challenge: code_challenge,
|
103
|
-
code_challenge_method: code_challenge_method,
|
104
|
-
expires_at: 10.minutes.from_now
|
105
|
-
)
|
106
|
-
end
|
107
|
-
|
108
|
-
# Create access token
|
109
|
-
def self.create_access_token(client_id:, user_id:, scope:)
|
110
|
-
expires_in = ActionMCP.configuration.oauth_config&.dig("access_token_expires_in") || 3600
|
111
|
-
|
112
|
-
create!(
|
113
|
-
token: SecureRandom.urlsafe_base64(32),
|
114
|
-
token_type: ACCESS_TOKEN,
|
115
|
-
client_id: client_id,
|
116
|
-
user_id: user_id,
|
117
|
-
scope: scope,
|
118
|
-
expires_at: expires_in.seconds.from_now
|
119
|
-
)
|
120
|
-
end
|
121
|
-
|
122
|
-
# Create refresh token
|
123
|
-
def self.create_refresh_token(client_id:, user_id:, scope:, access_token:)
|
124
|
-
expires_in = ActionMCP.configuration.oauth_config&.dig("refresh_token_expires_in") || 7.days.to_i
|
125
|
-
|
126
|
-
create!(
|
127
|
-
token: SecureRandom.urlsafe_base64(32),
|
128
|
-
token_type: REFRESH_TOKEN,
|
129
|
-
client_id: client_id,
|
130
|
-
user_id: user_id,
|
131
|
-
scope: scope,
|
132
|
-
access_token: access_token,
|
133
|
-
expires_at: expires_in.seconds.from_now
|
134
|
-
)
|
135
|
-
end
|
136
|
-
|
137
|
-
# Clean up expired tokens
|
138
|
-
def self.cleanup_expired
|
139
|
-
expired.delete_all
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
@@ -1,28 +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,
|
9
|
-
:oauth_access_token)
|
10
|
-
add_column :action_mcp_sessions, :oauth_refresh_token, :string unless column_exists?(:action_mcp_sessions,
|
11
|
-
:oauth_refresh_token)
|
12
|
-
add_column :action_mcp_sessions, :oauth_token_expires_at, :datetime unless column_exists?(:action_mcp_sessions,
|
13
|
-
:oauth_token_expires_at)
|
14
|
-
add_column :action_mcp_sessions, :oauth_user_context, json_type unless column_exists?(:action_mcp_sessions,
|
15
|
-
:oauth_user_context)
|
16
|
-
add_column :action_mcp_sessions, :authentication_method, :string, default: 'none' unless column_exists?(
|
17
|
-
:action_mcp_sessions, :authentication_method
|
18
|
-
)
|
19
|
-
|
20
|
-
# Add indexes for performance
|
21
|
-
add_index :action_mcp_sessions, :oauth_access_token, unique: true unless index_exists?(:action_mcp_sessions,
|
22
|
-
:oauth_access_token)
|
23
|
-
add_index :action_mcp_sessions, :oauth_token_expires_at unless index_exists?(:action_mcp_sessions,
|
24
|
-
:oauth_token_expires_at)
|
25
|
-
add_index :action_mcp_sessions, :authentication_method unless index_exists?(:action_mcp_sessions,
|
26
|
-
:authentication_method)
|
27
|
-
end
|
28
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
|
4
|
-
def change
|
5
|
-
create_table :action_mcp_oauth_clients do |t|
|
6
|
-
t.string :client_id, null: false, index: { unique: true }
|
7
|
-
t.string :client_secret
|
8
|
-
t.string :client_name
|
9
|
-
|
10
|
-
# Store arrays as JSON for database compatibility
|
11
|
-
if connection.adapter_name.downcase.include?('postgresql')
|
12
|
-
t.text :redirect_uris, array: true, default: []
|
13
|
-
t.text :grant_types, array: true, default: [ 'authorization_code' ]
|
14
|
-
t.text :response_types, array: true, default: [ 'code' ]
|
15
|
-
else
|
16
|
-
# For SQLite and other databases, use JSON
|
17
|
-
t.json :redirect_uris, default: []
|
18
|
-
t.json :grant_types, default: [ 'authorization_code' ]
|
19
|
-
t.json :response_types, default: [ 'code' ]
|
20
|
-
end
|
21
|
-
|
22
|
-
t.string :token_endpoint_auth_method, default: 'client_secret_basic'
|
23
|
-
t.text :scope
|
24
|
-
t.boolean :active, default: true
|
25
|
-
|
26
|
-
# Registration metadata
|
27
|
-
t.integer :client_id_issued_at
|
28
|
-
t.integer :client_secret_expires_at
|
29
|
-
t.string :registration_access_token # OAuth 2.1 Dynamic Client Registration
|
30
|
-
|
31
|
-
# Additional metadata as JSON for database compatibility
|
32
|
-
if connection.adapter_name.downcase.include?('postgresql')
|
33
|
-
t.jsonb :metadata, default: {}
|
34
|
-
else
|
35
|
-
t.json :metadata, default: {}
|
36
|
-
end
|
37
|
-
|
38
|
-
t.timestamps
|
39
|
-
end
|
40
|
-
|
41
|
-
add_index :action_mcp_oauth_clients, :active
|
42
|
-
add_index :action_mcp_oauth_clients, :client_id_issued_at
|
43
|
-
end
|
44
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class CreateActionMCPOAuthTokens < ActiveRecord::Migration[7.2]
|
4
|
-
def change
|
5
|
-
create_table :action_mcp_oauth_tokens do |t|
|
6
|
-
t.string :token, null: false, index: { unique: true }
|
7
|
-
t.string :token_type, null: false # 'access_token', 'refresh_token', 'authorization_code'
|
8
|
-
t.string :client_id, null: false
|
9
|
-
t.string :user_id
|
10
|
-
t.text :scope
|
11
|
-
t.datetime :expires_at
|
12
|
-
t.boolean :revoked, default: false
|
13
|
-
|
14
|
-
# For authorization codes
|
15
|
-
t.string :redirect_uri
|
16
|
-
t.string :code_challenge
|
17
|
-
t.string :code_challenge_method
|
18
|
-
|
19
|
-
# For refresh tokens
|
20
|
-
t.string :access_token # Reference to associated access token
|
21
|
-
|
22
|
-
# Additional data - use JSON for database compatibility
|
23
|
-
if connection.adapter_name.downcase.include?('postgresql')
|
24
|
-
t.jsonb :metadata, default: {}
|
25
|
-
else
|
26
|
-
t.json :metadata, default: {}
|
27
|
-
end
|
28
|
-
|
29
|
-
t.timestamps
|
30
|
-
end
|
31
|
-
|
32
|
-
add_index :action_mcp_oauth_tokens, :token_type
|
33
|
-
add_index :action_mcp_oauth_tokens, :client_id
|
34
|
-
add_index :action_mcp_oauth_tokens, :user_id
|
35
|
-
add_index :action_mcp_oauth_tokens, :expires_at
|
36
|
-
add_index :action_mcp_oauth_tokens, :revoked
|
37
|
-
add_index :action_mcp_oauth_tokens, %i[token_type expires_at]
|
38
|
-
end
|
39
|
-
end
|
@@ -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
|