mcp-auth 0.1.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 +7 -0
- data/CHANGELOG.md +43 -0
- data/CONTRIBUTING.md +107 -0
- data/LICENSE.txt +21 -0
- data/README.md +869 -0
- data/Rakefile +8 -0
- data/app/controllers/mcp/auth/oauth_controller.rb +494 -0
- data/app/controllers/mcp/auth/well_known_controller.rb +147 -0
- data/app/models/mcp/auth/access_token.rb +30 -0
- data/app/models/mcp/auth/authorization_code.rb +33 -0
- data/app/models/mcp/auth/oauth_client.rb +60 -0
- data/app/models/mcp/auth/refresh_token.rb +32 -0
- data/app/views/mcp/auth/consent.html.erb +527 -0
- data/config/routes.rb +43 -0
- data/lib/generators/mcp/auth/install_generator.rb +80 -0
- data/lib/generators/mcp/auth/templates/README +114 -0
- data/lib/generators/mcp/auth/templates/create_access_tokens.rb.erb +23 -0
- data/lib/generators/mcp/auth/templates/create_authorization_codes.rb.erb +26 -0
- data/lib/generators/mcp/auth/templates/create_oauth_clients.rb.erb +22 -0
- data/lib/generators/mcp/auth/templates/create_refresh_tokens.rb.erb +22 -0
- data/lib/generators/mcp/auth/templates/initializer.rb +199 -0
- data/lib/generators/mcp/auth/templates/views/consent.html.erb +527 -0
- data/lib/mcp/auth/engine.rb +32 -0
- data/lib/mcp/auth/scope_registry.rb +113 -0
- data/lib/mcp/auth/services/authorization_service.rb +102 -0
- data/lib/mcp/auth/services/token_service.rb +230 -0
- data/lib/mcp/auth/version.rb +7 -0
- data/lib/mcp/auth.rb +109 -0
- data/lib/tasks/mcp_auth_tasks.rake +89 -0
- metadata +254 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mcp
|
|
4
|
+
module Auth
|
|
5
|
+
module Services
|
|
6
|
+
class AuthorizationService
|
|
7
|
+
class << self
|
|
8
|
+
# Generate authorization code with PKCE support
|
|
9
|
+
def generate_authorization_code(params, user:, org:)
|
|
10
|
+
code = SecureRandom.hex(32)
|
|
11
|
+
|
|
12
|
+
# Use provided scope or default to all registered scopes
|
|
13
|
+
scope = params[:scope].presence || Mcp::Auth::ScopeRegistry.default_scope_string
|
|
14
|
+
|
|
15
|
+
authorization_code = Mcp::Auth::AuthorizationCode.create!(
|
|
16
|
+
code: code,
|
|
17
|
+
client_id: params[:client_id],
|
|
18
|
+
redirect_uri: params[:redirect_uri],
|
|
19
|
+
code_challenge: params[:code_challenge],
|
|
20
|
+
code_challenge_method: params[:code_challenge_method],
|
|
21
|
+
resource: params[:resource],
|
|
22
|
+
scope: scope,
|
|
23
|
+
user: user,
|
|
24
|
+
org: org,
|
|
25
|
+
expires_at: authorization_code_lifetime.minutes.from_now
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
Rails.logger.info "[AuthorizationService] Authorization code generated for user #{user.id}"
|
|
29
|
+
authorization_code.code
|
|
30
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
31
|
+
Rails.logger.error "[AuthorizationService] Failed to create authorization code: #{e.message}"
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Validate authorization code without consuming it
|
|
36
|
+
def validate_authorization_code(code)
|
|
37
|
+
return nil if code.blank?
|
|
38
|
+
|
|
39
|
+
authorization_code = Mcp::Auth::AuthorizationCode.active.find_by(code: code)
|
|
40
|
+
return nil unless authorization_code
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
client_id: authorization_code.client_id,
|
|
44
|
+
redirect_uri: authorization_code.redirect_uri,
|
|
45
|
+
code_challenge: authorization_code.code_challenge,
|
|
46
|
+
code_challenge_method: authorization_code.code_challenge_method,
|
|
47
|
+
resource: authorization_code.resource,
|
|
48
|
+
scope: authorization_code.scope,
|
|
49
|
+
user_id: authorization_code.user_id,
|
|
50
|
+
org_id: authorization_code.org_id,
|
|
51
|
+
created_at: authorization_code.created_at.to_i
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Consume authorization code (one-time use)
|
|
56
|
+
def consume_authorization_code(code)
|
|
57
|
+
authorization_code = Mcp::Auth::AuthorizationCode.find_by(code: code)
|
|
58
|
+
return nil unless authorization_code
|
|
59
|
+
|
|
60
|
+
code_data = {
|
|
61
|
+
client_id: authorization_code.client_id,
|
|
62
|
+
redirect_uri: authorization_code.redirect_uri,
|
|
63
|
+
code_challenge: authorization_code.code_challenge,
|
|
64
|
+
code_challenge_method: authorization_code.code_challenge_method,
|
|
65
|
+
resource: authorization_code.resource,
|
|
66
|
+
scope: authorization_code.scope,
|
|
67
|
+
user_id: authorization_code.user_id,
|
|
68
|
+
org_id: authorization_code.org_id,
|
|
69
|
+
created_at: authorization_code.created_at.to_i
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
authorization_code.destroy
|
|
73
|
+
Rails.logger.info "[AuthorizationService] Authorization code consumed"
|
|
74
|
+
code_data
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Validate PKCE challenge (RFC 7636)
|
|
78
|
+
def validate_pkce?(code_challenge, code_verifier)
|
|
79
|
+
return false if code_verifier.blank? || code_challenge.blank?
|
|
80
|
+
|
|
81
|
+
# S256 method: BASE64URL(SHA256(code_verifier))
|
|
82
|
+
computed_challenge = Base64.urlsafe_encode64(
|
|
83
|
+
Digest::SHA256.digest(code_verifier),
|
|
84
|
+
padding: false
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
ActiveSupport::SecurityUtils.secure_compare(computed_challenge, code_challenge)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
Rails.logger.error "[AuthorizationService] PKCE validation error: #{e.message}"
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def authorization_code_lifetime
|
|
96
|
+
Rails.application.config.mcp_auth.authorization_code_lifetime || 30
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mcp
|
|
4
|
+
module Auth
|
|
5
|
+
module Services
|
|
6
|
+
class TokenService
|
|
7
|
+
class << self
|
|
8
|
+
# Validate access token with optional resource verification (RFC 8707)
|
|
9
|
+
def validate_access_token(token, resource: nil)
|
|
10
|
+
return nil if token.blank?
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
payload = JWT.decode(token, oauth_secret, true, { algorithm: 'HS256' }).first
|
|
14
|
+
|
|
15
|
+
# Check expiration manually to ensure proper handling
|
|
16
|
+
if payload['exp']
|
|
17
|
+
return nil if payload['exp'] <= Time.current.to_i
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Validate audience if resource provided (RFC 8707 compliance)
|
|
21
|
+
if resource && payload['aud'].present?
|
|
22
|
+
unless audience_matches?(payload['aud'], resource)
|
|
23
|
+
Rails.logger.warn "[TokenService] Token audience mismatch: expected #{resource}, got #{payload['aud']}"
|
|
24
|
+
return nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
payload.symbolize_keys
|
|
29
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature => e
|
|
30
|
+
Rails.logger.debug "[TokenService] Token validation failed: #{e.message}"
|
|
31
|
+
nil
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Rails.logger.error "[TokenService] Token validation error: #{e.message}"
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Generate JWT access token with proper audience binding
|
|
39
|
+
def generate_access_token(data, base_url:)
|
|
40
|
+
user_data = fetch_user_data(data)
|
|
41
|
+
|
|
42
|
+
# RFC 8707: Use provided resource or default to MCP API endpoint
|
|
43
|
+
audience = normalize_resource_uri(data[:resource].presence || "#{base_url}/mcp")
|
|
44
|
+
|
|
45
|
+
# Calculate expiration time
|
|
46
|
+
exp_time = data[:expires_at] ? data[:expires_at].to_i : (Time.current.to_i + token_lifetime)
|
|
47
|
+
|
|
48
|
+
payload = {
|
|
49
|
+
iss: base_url,
|
|
50
|
+
aud: audience,
|
|
51
|
+
sub: data[:user_id].to_s,
|
|
52
|
+
org: data[:org_id]&.to_s,
|
|
53
|
+
client_id: data[:client_id],
|
|
54
|
+
email: user_data[:email],
|
|
55
|
+
scope: data[:scope],
|
|
56
|
+
api_key_id: user_data[:api_key_id],
|
|
57
|
+
api_key_secret: user_data[:api_key_secret],
|
|
58
|
+
iat: Time.current.to_i,
|
|
59
|
+
exp: exp_time
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
token = JWT.encode(payload, oauth_secret, 'HS256')
|
|
63
|
+
|
|
64
|
+
# Store token in database for revocation support
|
|
65
|
+
store_access_token(token, data, audience)
|
|
66
|
+
|
|
67
|
+
token
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
Rails.logger.error "[TokenService] Failed to generate access token: #{e.message}"
|
|
70
|
+
raise
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Generate refresh token
|
|
74
|
+
def generate_refresh_token(data)
|
|
75
|
+
refresh_token = SecureRandom.hex(32)
|
|
76
|
+
|
|
77
|
+
# Use provided expires_at or default
|
|
78
|
+
expires_at = data[:expires_at] || refresh_token_lifetime.seconds.from_now
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
Mcp::Auth::RefreshToken.create!(
|
|
82
|
+
token: refresh_token,
|
|
83
|
+
client_id: data[:client_id],
|
|
84
|
+
scope: data[:scope],
|
|
85
|
+
user_id: data[:user_id],
|
|
86
|
+
org_id: data[:org_id],
|
|
87
|
+
expires_at: expires_at
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
Rails.logger.info "[TokenService] Refresh token created for user #{data[:user_id]}"
|
|
91
|
+
refresh_token
|
|
92
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
93
|
+
Rails.logger.error "[TokenService] Failed to create refresh token: #{e.message}"
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Validate refresh token
|
|
99
|
+
def validate_refresh_token(refresh_token)
|
|
100
|
+
return nil if refresh_token.blank?
|
|
101
|
+
|
|
102
|
+
token_record = Mcp::Auth::RefreshToken.find_by(token: refresh_token)
|
|
103
|
+
return nil unless token_record
|
|
104
|
+
|
|
105
|
+
# Check if token is expired
|
|
106
|
+
return nil if token_record.expires_at < Time.current
|
|
107
|
+
|
|
108
|
+
Rails.logger.info "[TokenService] Refresh token validated for user #{token_record.user_id}"
|
|
109
|
+
{
|
|
110
|
+
client_id: token_record.client_id,
|
|
111
|
+
scope: token_record.scope,
|
|
112
|
+
user_id: token_record.user_id,
|
|
113
|
+
org_id: token_record.org_id
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Revoke refresh token (RFC 7009)
|
|
118
|
+
def revoke_refresh_token(refresh_token)
|
|
119
|
+
return false if refresh_token.blank?
|
|
120
|
+
|
|
121
|
+
token_record = Mcp::Auth::RefreshToken.find_by(token: refresh_token)
|
|
122
|
+
return false unless token_record
|
|
123
|
+
|
|
124
|
+
token_record.destroy
|
|
125
|
+
Rails.logger.info "[TokenService] Refresh token revoked"
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Generate complete token response
|
|
130
|
+
def generate_token_response(data, base_url:)
|
|
131
|
+
access_token = generate_access_token(data, base_url: base_url)
|
|
132
|
+
refresh_token = generate_refresh_token(data)
|
|
133
|
+
|
|
134
|
+
response = {
|
|
135
|
+
access_token: access_token,
|
|
136
|
+
token_type: 'Bearer',
|
|
137
|
+
expires_in: token_lifetime,
|
|
138
|
+
scope: data[:scope]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
response[:refresh_token] = refresh_token if refresh_token
|
|
142
|
+
response
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
Rails.logger.error "[TokenService] Failed to generate token response: #{e.message}"
|
|
145
|
+
raise
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def oauth_secret
|
|
151
|
+
secret = Mcp::Auth.configuration&.oauth_secret
|
|
152
|
+
secret.presence || Rails.application.secret_key_base
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def token_lifetime
|
|
156
|
+
Mcp::Auth.configuration&.access_token_lifetime || 3600
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def refresh_token_lifetime
|
|
160
|
+
Mcp::Auth.configuration&.refresh_token_lifetime || 2_592_000
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# RFC 8707: Normalize resource URI (remove trailing slash, lowercase scheme/host)
|
|
164
|
+
def normalize_resource_uri(uri)
|
|
165
|
+
parsed = URI.parse(uri)
|
|
166
|
+
normalized = "#{parsed.scheme.downcase}://#{parsed.host.downcase}"
|
|
167
|
+
normalized += ":#{parsed.port}" if parsed.port && !default_port?(parsed)
|
|
168
|
+
normalized += parsed.path.chomp('/') if parsed.path.present? && parsed.path != '/'
|
|
169
|
+
normalized
|
|
170
|
+
rescue URI::InvalidURIError => e
|
|
171
|
+
Rails.logger.warn "[TokenService] Invalid resource URI: #{uri} - #{e.message}"
|
|
172
|
+
uri
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def default_port?(parsed_uri)
|
|
176
|
+
(parsed_uri.scheme == 'http' && parsed_uri.port == 80) ||
|
|
177
|
+
(parsed_uri.scheme == 'https' && parsed_uri.port == 443)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# RFC 8707: Check if token audience matches requested resource
|
|
181
|
+
def audience_matches?(token_audience, resource)
|
|
182
|
+
normalized_audience = normalize_resource_uri(token_audience)
|
|
183
|
+
normalized_resource = normalize_resource_uri(resource)
|
|
184
|
+
|
|
185
|
+
# Exact match or audience is a prefix of resource
|
|
186
|
+
normalized_audience == normalized_resource ||
|
|
187
|
+
normalized_resource.start_with?(normalized_audience)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def store_access_token(token, data, audience)
|
|
191
|
+
# Use provided expires_at or default
|
|
192
|
+
expires_at = data[:expires_at] || token_lifetime.seconds.from_now
|
|
193
|
+
|
|
194
|
+
Mcp::Auth::AccessToken.create!(
|
|
195
|
+
token: token,
|
|
196
|
+
client_id: data[:client_id],
|
|
197
|
+
resource: audience,
|
|
198
|
+
scope: data[:scope],
|
|
199
|
+
user_id: data[:user_id],
|
|
200
|
+
org_id: data[:org_id],
|
|
201
|
+
expires_at: expires_at
|
|
202
|
+
)
|
|
203
|
+
Rails.logger.info "[TokenService] Access token stored for user #{data[:user_id]}"
|
|
204
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
205
|
+
Rails.logger.error "[TokenService] Failed to store access token: #{e.message}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def fetch_user_data(data)
|
|
209
|
+
if Mcp::Auth.configuration&.fetch_user_data
|
|
210
|
+
Mcp::Auth.configuration.fetch_user_data.call(data)
|
|
211
|
+
else
|
|
212
|
+
default_fetch_user_data(data[:user_id])
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def default_fetch_user_data(user_id)
|
|
217
|
+
user = User.find(user_id)
|
|
218
|
+
{
|
|
219
|
+
email: user.email,
|
|
220
|
+
api_key_id: nil,
|
|
221
|
+
api_key_secret: nil
|
|
222
|
+
}
|
|
223
|
+
rescue ActiveRecord::RecordNotFound
|
|
224
|
+
{ email: 'unknown@example.com', api_key_id: nil, api_key_secret: nil }
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
data/lib/mcp/auth.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mcp/auth/version'
|
|
4
|
+
require 'mcp/auth/engine'
|
|
5
|
+
require 'mcp/auth/services/token_service'
|
|
6
|
+
require 'mcp/auth/services/authorization_service'
|
|
7
|
+
|
|
8
|
+
module Mcp
|
|
9
|
+
module Auth
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.configure
|
|
17
|
+
self.configuration ||= Configuration.new
|
|
18
|
+
yield(configuration)
|
|
19
|
+
|
|
20
|
+
# Also set on Rails.application.config for controllers to access
|
|
21
|
+
Rails.application.config.mcp_auth = configuration if defined?(Rails)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Configuration
|
|
25
|
+
attr_accessor :oauth_secret,
|
|
26
|
+
:authorization_server_url,
|
|
27
|
+
:access_token_lifetime,
|
|
28
|
+
:refresh_token_lifetime,
|
|
29
|
+
:authorization_code_lifetime,
|
|
30
|
+
:fetch_user_data,
|
|
31
|
+
:current_user_method,
|
|
32
|
+
:current_org_method,
|
|
33
|
+
:consent_view_path,
|
|
34
|
+
:use_custom_consent_view,
|
|
35
|
+
:mcp_server_path,
|
|
36
|
+
:mcp_docs_url,
|
|
37
|
+
:validate_scope_for_user
|
|
38
|
+
|
|
39
|
+
def initialize
|
|
40
|
+
@oauth_secret = nil
|
|
41
|
+
@authorization_server_url = nil
|
|
42
|
+
@access_token_lifetime = 3600 # 1 hour
|
|
43
|
+
@refresh_token_lifetime = 2_592_000 # 30 days
|
|
44
|
+
@authorization_code_lifetime = 1800 # 30 minutes
|
|
45
|
+
@fetch_user_data = nil
|
|
46
|
+
@current_user_method = :current_user
|
|
47
|
+
@current_org_method = nil
|
|
48
|
+
@consent_view_path = 'mcp/auth/consent'
|
|
49
|
+
@use_custom_consent_view = false
|
|
50
|
+
@mcp_server_path = '/mcp'
|
|
51
|
+
@mcp_docs_url = nil
|
|
52
|
+
@validate_scope_for_user = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Register a custom scope for your application
|
|
56
|
+
def register_scope(scope_key, name:, description:, required: false)
|
|
57
|
+
Mcp::Auth::ScopeRegistry.register_scope(
|
|
58
|
+
scope_key,
|
|
59
|
+
name: name,
|
|
60
|
+
description: description,
|
|
61
|
+
required: required
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get MCP documentation URL
|
|
66
|
+
def documentation_url(base_url = nil)
|
|
67
|
+
return @mcp_docs_url if @mcp_docs_url.present? && @mcp_docs_url.start_with?('http')
|
|
68
|
+
|
|
69
|
+
docs_path = @mcp_docs_url.presence || "#{@mcp_server_path}/docs"
|
|
70
|
+
base_url ? "#{base_url}#{docs_path}" : docs_path
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Helper methods for controllers
|
|
75
|
+
module ControllerHelpers
|
|
76
|
+
def mcp_user_id
|
|
77
|
+
request.env['mcp.user_id']
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mcp_org_id
|
|
81
|
+
request.env['mcp.org_id']
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def mcp_email
|
|
85
|
+
request.env['mcp.email']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def mcp_token
|
|
89
|
+
request.env['mcp.token']
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def mcp_scope
|
|
93
|
+
request.env['mcp.scope']
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def mcp_api_key
|
|
97
|
+
request.env['mcp.api_key']
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mcp_authenticated?
|
|
101
|
+
mcp_user_id.present?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
require 'mcp/auth/scope_registry'
|
|
108
|
+
require 'mcp/auth/services/token_service'
|
|
109
|
+
require 'mcp/auth/services/authorization_service'
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :mcp_auth do
|
|
4
|
+
desc "Clean up expired tokens and authorization codes"
|
|
5
|
+
task cleanup: :environment do
|
|
6
|
+
puts "Cleaning up expired MCP Auth tokens..."
|
|
7
|
+
|
|
8
|
+
expired_auth_codes = Mcp::Auth::AuthorizationCode.cleanup_expired
|
|
9
|
+
puts " - Removed #{expired_auth_codes} expired authorization codes"
|
|
10
|
+
|
|
11
|
+
expired_access_tokens = Mcp::Auth::AccessToken.cleanup_expired
|
|
12
|
+
puts " - Removed #{expired_access_tokens} expired access tokens"
|
|
13
|
+
|
|
14
|
+
expired_refresh_tokens = Mcp::Auth::RefreshToken.cleanup_expired
|
|
15
|
+
puts " - Removed #{expired_refresh_tokens} expired refresh tokens"
|
|
16
|
+
|
|
17
|
+
puts "Cleanup complete!"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Show MCP Auth statistics"
|
|
21
|
+
task stats: :environment do
|
|
22
|
+
puts "\nMCP Auth Statistics"
|
|
23
|
+
puts "=" * 50
|
|
24
|
+
|
|
25
|
+
puts "\nOAuth Clients:"
|
|
26
|
+
puts " Total: #{Mcp::Auth::OauthClient.count}"
|
|
27
|
+
|
|
28
|
+
puts "\nAuthorization Codes:"
|
|
29
|
+
puts " Active: #{Mcp::Auth::AuthorizationCode.active.count}"
|
|
30
|
+
puts " Expired: #{Mcp::Auth::AuthorizationCode.expired.count}"
|
|
31
|
+
puts " Total: #{Mcp::Auth::AuthorizationCode.count}"
|
|
32
|
+
|
|
33
|
+
puts "\nAccess Tokens:"
|
|
34
|
+
puts " Active: #{Mcp::Auth::AccessToken.active.count}"
|
|
35
|
+
puts " Expired: #{Mcp::Auth::AccessToken.expired.count}"
|
|
36
|
+
puts " Total: #{Mcp::Auth::AccessToken.count}"
|
|
37
|
+
|
|
38
|
+
puts "\nRefresh Tokens:"
|
|
39
|
+
puts " Active: #{Mcp::Auth::RefreshToken.active.count}"
|
|
40
|
+
puts " Expired: #{Mcp::Auth::RefreshToken.expired.count}"
|
|
41
|
+
puts " Total: #{Mcp::Auth::RefreshToken.count}"
|
|
42
|
+
|
|
43
|
+
puts "\n" + "=" * 50
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Revoke all tokens for a specific client"
|
|
47
|
+
task :revoke_client_tokens, [:client_id] => :environment do |_t, args|
|
|
48
|
+
client_id = args[:client_id]
|
|
49
|
+
|
|
50
|
+
if client_id.blank?
|
|
51
|
+
puts "Error: Please provide a client_id"
|
|
52
|
+
puts "Usage: rake mcp_auth:revoke_client_tokens[CLIENT_ID]"
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts "Revoking all tokens for client: #{client_id}"
|
|
57
|
+
|
|
58
|
+
auth_codes = Mcp::Auth::AuthorizationCode.where(client_id: client_id).delete_all
|
|
59
|
+
access_tokens = Mcp::Auth::AccessToken.where(client_id: client_id).delete_all
|
|
60
|
+
refresh_tokens = Mcp::Auth::RefreshToken.where(client_id: client_id).delete_all
|
|
61
|
+
|
|
62
|
+
puts " - Removed #{auth_codes} authorization codes"
|
|
63
|
+
puts " - Removed #{access_tokens} access tokens"
|
|
64
|
+
puts " - Removed #{refresh_tokens} refresh tokens"
|
|
65
|
+
puts "Complete!"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "Revoke all tokens for a specific user"
|
|
69
|
+
task :revoke_user_tokens, [:user_id] => :environment do |_t, args|
|
|
70
|
+
user_id = args[:user_id]
|
|
71
|
+
|
|
72
|
+
if user_id.blank?
|
|
73
|
+
puts "Error: Please provide a user_id"
|
|
74
|
+
puts "Usage: rake mcp_auth:revoke_user_tokens[USER_ID]"
|
|
75
|
+
exit 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
puts "Revoking all tokens for user: #{user_id}"
|
|
79
|
+
|
|
80
|
+
auth_codes = Mcp::Auth::AuthorizationCode.where(user_id: user_id).delete_all
|
|
81
|
+
access_tokens = Mcp::Auth::AccessToken.where(user_id: user_id).delete_all
|
|
82
|
+
refresh_tokens = Mcp::Auth::RefreshToken.where(user_id: user_id).delete_all
|
|
83
|
+
|
|
84
|
+
puts " - Removed #{auth_codes} authorization codes"
|
|
85
|
+
puts " - Removed #{access_tokens} access tokens"
|
|
86
|
+
puts " - Removed #{refresh_tokens} refresh tokens"
|
|
87
|
+
puts "Complete!"
|
|
88
|
+
end
|
|
89
|
+
end
|