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.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,494 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mcp
4
+ module Auth
5
+ class OauthController < ApplicationController
6
+ skip_before_action :verify_authenticity_token, only: %i[token register revoke introspect userinfo]
7
+ before_action :set_cors_headers
8
+ before_action :handle_options_request
9
+ before_action :require_https, only: %i[authorize token]
10
+
11
+ # OAuth 2.1 Authorization endpoint (GET/POST)
12
+ def authorize
13
+ Rails.logger.info "[OAuth] Authorization request: #{params.inspect}"
14
+
15
+ unless valid_authorization_params?
16
+ return render_error('invalid_request', 'Missing or invalid required parameters')
17
+ end
18
+
19
+ if user_signed_in?
20
+ handle_signed_in_user
21
+ else
22
+ redirect_to_login
23
+ end
24
+ end
25
+
26
+ # Consent approval endpoint
27
+ def approve
28
+ unless user_signed_in?
29
+ return redirect_to main_app.new_user_session_path
30
+ end
31
+
32
+ unless valid_authorization_params?
33
+ return render_error('invalid_request', 'Missing required parameters')
34
+ end
35
+
36
+ if params[:approved] == 'true'
37
+ # Get selected scopes from checkboxes
38
+ selected_scopes = Array(params[:scopes]).compact.reject(&:blank?)
39
+
40
+ Rails.logger.info "[OAuth] User selected scopes: #{selected_scopes.inspect}"
41
+
42
+ # Validate selected scopes
43
+ if selected_scopes.blank?
44
+ Rails.logger.warn "[OAuth] No scopes selected"
45
+ return render_error('invalid_request', 'At least one scope must be selected')
46
+ end
47
+
48
+ # Get originally requested scopes
49
+ requested_scopes = params[:scope]&.split || []
50
+
51
+ # Get required scopes from the requested list
52
+ required_scopes = get_required_scopes(requested_scopes)
53
+
54
+ # Check all required scopes are selected
55
+ missing_required = required_scopes - selected_scopes
56
+ if missing_required.any?
57
+ Rails.logger.warn "[OAuth] Missing required scopes: #{missing_required.join(', ')}"
58
+ return render_error('invalid_request', 'Required scopes must be selected')
59
+ end
60
+ approved_scopes = Mcp::Auth::ScopeRegistry.validate_scopes(selected_scopes)
61
+ # Generate authorization code with ONLY approved scopes
62
+ approved_scope_string = approved_scopes.join(' ')
63
+ generate_and_redirect_with_code(approved_scope_string)
64
+ else
65
+ redirect_with_error('access_denied', 'User denied the request')
66
+ end
67
+ end
68
+
69
+ # OAuth 2.1 Token endpoint
70
+ def token
71
+ case params[:grant_type]
72
+ when 'authorization_code'
73
+ handle_authorization_code_grant
74
+ when 'refresh_token'
75
+ handle_refresh_token_grant
76
+ else
77
+ render_error('unsupported_grant_type', 'Grant type not supported')
78
+ end
79
+ end
80
+
81
+ # RFC 7591: Dynamic Client Registration
82
+ def register
83
+ Rails.logger.info "[OAuth] Client registration request"
84
+
85
+ begin
86
+ client_data = build_client_registration
87
+ oauth_client = Mcp::Auth::OauthClient.create!(client_data)
88
+
89
+ Rails.logger.info "[OAuth] Client registered: #{oauth_client.client_id}"
90
+ render json: format_client_response(oauth_client), content_type: 'application/json'
91
+ rescue ActiveRecord::RecordInvalid => e
92
+ render_error('invalid_client_metadata', e.message)
93
+ rescue ArgumentError => e
94
+ render_error('invalid_request', e.message)
95
+ rescue StandardError => e
96
+ Rails.logger.error "[OAuth] Registration error: #{e.message}"
97
+ render_error('server_error', 'An unexpected error occurred')
98
+ end
99
+ end
100
+
101
+ # RFC 7009: Token Revocation
102
+ def revoke
103
+ token = params[:token]
104
+
105
+ if token.blank?
106
+ return render_error('invalid_request', 'Token parameter is required')
107
+ end
108
+
109
+ # Try refresh token first
110
+ revoked = Services::TokenService.revoke_refresh_token(token)
111
+
112
+ # Try access token if not found
113
+ unless revoked
114
+ access_token = Mcp::Auth::AccessToken.find_by(token: token)
115
+ if access_token
116
+ access_token.destroy
117
+ revoked = true
118
+ end
119
+ end
120
+
121
+ # RFC 7009: Always return 200 OK
122
+ Rails.logger.info "[OAuth] Token revocation: #{revoked ? 'success' : 'not found'}"
123
+ head :ok
124
+ end
125
+
126
+ # RFC 7662: Token Introspection
127
+ def introspect
128
+ token = params[:token]
129
+
130
+ if token.blank?
131
+ return render json: { active: false }, content_type: 'application/json'
132
+ end
133
+
134
+ # Try as JWT access token
135
+ payload = Services::TokenService.validate_access_token(token)
136
+
137
+ response = if payload
138
+ build_access_token_introspection(payload)
139
+ else
140
+ # Try as refresh token
141
+ refresh_data = Services::TokenService.validate_refresh_token(token)
142
+ refresh_data ? build_refresh_token_introspection(refresh_data) : { active: false }
143
+ end
144
+
145
+ render json: response, content_type: 'application/json'
146
+ end
147
+
148
+ # OpenID Connect UserInfo Endpoint
149
+ def userinfo
150
+ auth_header = request.headers['Authorization']
151
+
152
+ unless auth_header&.start_with?('Bearer ')
153
+ return render json: { error: 'invalid_token' }, status: :unauthorized
154
+ end
155
+
156
+ token = auth_header.split(' ', 2).last
157
+ payload = Services::TokenService.validate_access_token(token)
158
+
159
+ unless payload
160
+ return render json: { error: 'invalid_token' }, status: :unauthorized
161
+ end
162
+
163
+ user_info = {
164
+ sub: payload[:sub],
165
+ email: payload[:email],
166
+ email_verified: true,
167
+ name: payload[:email],
168
+ preferred_username: payload[:email]
169
+ }
170
+ user_info[:org] = payload[:org] if payload[:org]
171
+
172
+ render json: user_info, content_type: 'application/json'
173
+ end
174
+
175
+ private
176
+
177
+ # === Validation ===
178
+
179
+ def valid_authorization_params?
180
+ params[:response_type] == 'code' &&
181
+ params[:client_id].present? &&
182
+ params[:redirect_uri].present? &&
183
+ params[:code_challenge].present? &&
184
+ params[:code_challenge_method] == 'S256'
185
+ end
186
+
187
+ # === Authorization Flow ===
188
+
189
+ def handle_signed_in_user
190
+ if params[:approved] == 'true'
191
+ generate_and_redirect_with_code
192
+ else
193
+ show_consent_screen
194
+ end
195
+ end
196
+
197
+ def redirect_to_login
198
+ session[:oauth_params] = request.query_parameters
199
+ redirect_to main_app.new_user_session_path
200
+ end
201
+
202
+ def generate_and_redirect_with_code(approved_scope = nil)
203
+ # Use approved scopes if provided, otherwise use requested scopes,
204
+ # otherwise use all registered scopes
205
+ final_scope = approved_scope.presence ||
206
+ params[:scope].presence ||
207
+ Mcp::Auth::ScopeRegistry.default_scope_string
208
+
209
+ Rails.logger.info "[OAuth] Generating auth code with scope: #{final_scope}"
210
+
211
+ # Create a new params hash with the final scope
212
+ auth_params = params.to_unsafe_h.merge(scope: final_scope)
213
+
214
+ # Pass the params with approved scope to authorization service
215
+ code = Services::AuthorizationService.generate_authorization_code(
216
+ auth_params,
217
+ user: current_user,
218
+ org: current_org
219
+ )
220
+
221
+ unless code
222
+ return render_error('server_error', 'Failed to generate authorization code')
223
+ end
224
+
225
+ redirect_with_code(code)
226
+ end
227
+
228
+ def redirect_with_code(code)
229
+ redirect_uri = URI.parse(params[:redirect_uri])
230
+ query_params = { code: code }
231
+ query_params[:state] = params[:state] if params[:state]
232
+ query_params[:iss] = authorization_server_url # OAuth 2.1
233
+
234
+ redirect_uri.query = URI.encode_www_form(query_params)
235
+ redirect_to redirect_uri.to_s, allow_other_host: true
236
+ end
237
+
238
+ def redirect_with_error(error, description)
239
+ redirect_uri = URI.parse(params[:redirect_uri])
240
+ query_params = { error: error, error_description: description }
241
+ query_params[:state] = params[:state] if params[:state]
242
+
243
+ redirect_uri.query = URI.encode_www_form(query_params)
244
+ redirect_to redirect_uri.to_s, allow_other_host: true
245
+ end
246
+
247
+ # === Token Grants ===
248
+
249
+ def handle_authorization_code_grant
250
+ code_data = Services::AuthorizationService.validate_authorization_code(params[:code])
251
+
252
+ unless code_data
253
+ return render_error('invalid_grant', 'Authorization code is invalid or expired')
254
+ end
255
+
256
+ # Validate PKCE
257
+ unless Services::AuthorizationService.validate_pkce?(code_data[:code_challenge], params[:code_verifier])
258
+ return render_error('invalid_grant', 'PKCE validation failed')
259
+ end
260
+
261
+ # Validate redirect URI
262
+ unless code_data[:redirect_uri] == params[:redirect_uri]
263
+ return render_error('invalid_grant', 'Redirect URI mismatch')
264
+ end
265
+
266
+ # Use the APPROVED scope from the authorization code, not the original request
267
+ Rails.logger.info "[OAuth] Token generation using scope from auth code: #{code_data[:scope]}"
268
+
269
+ # Generate tokens with the APPROVED scope from authorization code
270
+ token_data = code_data.merge(resource: code_data[:resource] || params[:resource])
271
+ token_response = Services::TokenService.generate_token_response(
272
+ token_data, # This includes the approved :scope from authorization code
273
+ base_url: request.base_url
274
+ )
275
+
276
+ # Consume authorization code (one-time use)
277
+ Services::AuthorizationService.consume_authorization_code(params[:code])
278
+
279
+ render json: token_response, content_type: 'application/json'
280
+ end
281
+
282
+ def handle_refresh_token_grant
283
+ token_data = Services::TokenService.validate_refresh_token(params[:refresh_token])
284
+
285
+ unless token_data
286
+ return render_error('invalid_grant', 'Refresh token is invalid or expired')
287
+ end
288
+
289
+ # Include resource parameter if provided
290
+ token_data[:resource] = params[:resource] if params[:resource]
291
+
292
+ # Generate new tokens
293
+ token_response = Services::TokenService.generate_token_response(
294
+ token_data,
295
+ base_url: request.base_url
296
+ )
297
+
298
+ # Rotate refresh token (OAuth 2.1 requirement)
299
+ Services::TokenService.revoke_refresh_token(params[:refresh_token])
300
+
301
+ render json: token_response, content_type: 'application/json'
302
+ end
303
+
304
+ # === Client Registration ===
305
+
306
+ def build_client_registration
307
+ {
308
+ redirect_uris: extract_redirect_uris,
309
+ grant_types: params[:grant_types] || %w[authorization_code refresh_token],
310
+ response_types: params[:response_types] || %w[code],
311
+ scope: params[:scope] || Mcp::Auth::ScopeRegistry.default_scope_string,
312
+ client_name: params[:client_name] || 'MCP Client',
313
+ client_uri: params[:client_uri]
314
+ }
315
+ end
316
+
317
+ def extract_redirect_uris
318
+ uris = params[:redirect_uris] || []
319
+ Array(uris).uniq
320
+ end
321
+
322
+ def format_client_response(client)
323
+ {
324
+ client_id: client.client_id,
325
+ client_secret: client.client_secret,
326
+ client_id_issued_at: client.created_at.to_i,
327
+ client_secret_expires_at: 0,
328
+ redirect_uris: client.redirect_uris,
329
+ grant_types: client.grant_types,
330
+ response_types: client.response_types,
331
+ scope: client.scope,
332
+ token_endpoint_auth_method: 'client_secret_basic',
333
+ client_name: client.client_name,
334
+ client_uri: client.client_uri
335
+ }.compact
336
+ end
337
+
338
+ # === Token Introspection ===
339
+
340
+ def build_access_token_introspection(payload)
341
+ {
342
+ active: true,
343
+ client_id: payload[:client_id] || 'unknown',
344
+ username: payload[:email],
345
+ scope: payload[:scope],
346
+ exp: payload[:exp],
347
+ iat: payload[:iat],
348
+ sub: payload[:sub],
349
+ aud: payload[:aud],
350
+ iss: payload[:iss],
351
+ token_type: 'Bearer'
352
+ }
353
+ end
354
+
355
+ def build_refresh_token_introspection(data)
356
+ {
357
+ active: true,
358
+ client_id: data[:client_id],
359
+ scope: data[:scope],
360
+ token_type: 'refresh_token'
361
+ }
362
+ end
363
+
364
+ # === Consent Screen - ONLY ONE DEFINITION ===
365
+
366
+ def show_consent_screen
367
+ @client_name = get_client_name
368
+ @requested_scopes = parse_and_validate_scopes
369
+ @authorization_params = params.to_unsafe_h.slice(
370
+ :response_type, :client_id, :redirect_uri, :scope,
371
+ :state, :code_challenge, :code_challenge_method, :resource
372
+ )
373
+
374
+ if use_custom_consent_view?
375
+ render Rails.application.config.mcp_auth.consent_view_path, layout: 'application'
376
+ else
377
+ render 'mcp/auth/consent', layout: false
378
+ end
379
+ end
380
+
381
+ def use_custom_consent_view?
382
+ config = Rails.application.config.mcp_auth
383
+ config.use_custom_consent_view &&
384
+ template_exists?(config.consent_view_path)
385
+ rescue
386
+ false
387
+ end
388
+
389
+ def template_exists?(path)
390
+ lookup_context.exists?(path, [], false)
391
+ rescue
392
+ false
393
+ end
394
+
395
+ def get_client_name
396
+ client = Mcp::Auth::OauthClient.find_by_client_id(params[:client_id])
397
+ client&.client_name || params[:client_name] || params[:client_id] || 'Unknown Application'
398
+ end
399
+
400
+ def parse_and_validate_scopes
401
+ # Get requested scopes, or default to all registered scopes
402
+ scope_string = params[:scope].presence || Mcp::Auth::ScopeRegistry.default_scope_string
403
+ requested = scope_string.split
404
+
405
+ # Get ALL available scopes (what the gem owner registered)
406
+ all_available = Mcp::Auth::ScopeRegistry.available_scopes.keys
407
+
408
+ # Filter by user permissions if configured
409
+ if Mcp::Auth.configuration.validate_scope_for_user
410
+ all_available = all_available.select do |scope|
411
+ Mcp::Auth.configuration.validate_scope_for_user.call(
412
+ current_user,
413
+ current_org,
414
+ scope
415
+ )
416
+ end
417
+ end
418
+
419
+ # Format all available scopes for display
420
+ # Mark as pre-selected if they were in the original request
421
+ all_scopes = Mcp::Auth::ScopeRegistry.format_for_display(all_available)
422
+
423
+ # Add a flag to indicate if scope was originally requested
424
+ all_scopes.map do |scope_data|
425
+ scope_data.merge(
426
+ pre_selected: requested.include?(scope_data[:key])
427
+ )
428
+ end
429
+ end
430
+
431
+ # Get required scopes from requested scopes list
432
+ def get_required_scopes(requested_scopes)
433
+ validated = Mcp::Auth::ScopeRegistry.validate_scopes(requested_scopes)
434
+
435
+ validated.select do |scope|
436
+ metadata = Mcp::Auth::ScopeRegistry.scope_metadata(scope)
437
+ metadata[:required]
438
+ end
439
+ end
440
+
441
+ # === Headers & Security ===
442
+
443
+ def set_cors_headers
444
+ response.headers['Access-Control-Allow-Origin'] = '*'
445
+ response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
446
+ response.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
447
+ end
448
+
449
+ def handle_options_request
450
+ head :no_content if request.method == 'OPTIONS'
451
+ end
452
+
453
+ def require_https
454
+ return if request.ssl? || request.local? || Rails.env.development?
455
+
456
+ render_error('invalid_request', 'HTTPS required')
457
+ end
458
+
459
+ def authorization_server_url
460
+ config_url = Rails.application.config.mcp_auth.authorization_server_url
461
+ config_url.presence || "#{request.scheme}://#{request.host_with_port}"
462
+ end
463
+
464
+ # === Error Handling ===
465
+
466
+ def render_error(error, description)
467
+ error_response = {
468
+ error: error,
469
+ error_description: description
470
+ }
471
+
472
+ Rails.logger.error "[OAuth] Error: #{error_response.inspect}"
473
+ render json: error_response, status: :bad_request, content_type: 'application/json'
474
+ end
475
+
476
+ def current_org
477
+ # If current_org_method is nil in config, always return nil
478
+ return nil if Mcp::Auth.configuration.current_org_method.nil?
479
+
480
+ # Otherwise, call the configured method
481
+ method_name = Mcp::Auth.configuration.current_org_method
482
+
483
+ # If it's a proc, execute it in this controller's context
484
+ return instance_exec(&method_name) if method_name.respond_to?(:call)
485
+
486
+ # If it's a symbol/string, call it (from parent ApplicationController)
487
+ super if defined?(super)
488
+ rescue NoMethodError
489
+ # Method doesn't exist in parent, return nil
490
+ nil
491
+ end
492
+ end
493
+ end
494
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mcp
4
+ module Auth
5
+ class WellKnownController < ActionController::Base
6
+ skip_before_action :verify_authenticity_token
7
+ before_action :set_cors_headers
8
+ before_action :handle_options_request
9
+
10
+ # RFC 9728: OAuth 2.0 Protected Resource Metadata
11
+ def protected_resource
12
+ resource_url = canonical_resource_url
13
+
14
+ metadata = {
15
+ resource: resource_url,
16
+ authorization_servers: [authorization_server_url],
17
+ scopes_supported: Mcp::Auth::ScopeRegistry.available_scopes.keys,
18
+ bearer_methods_supported: %w[header],
19
+ resource_documentation: mcp_documentation_url,
20
+ resource_parameter_supported: true, # RFC 8707 support
21
+ authorization_response_iss_parameter_supported: true # OAuth 2.1
22
+ }
23
+
24
+ render json: metadata, status: :ok, content_type: 'application/json'
25
+ end
26
+
27
+ # RFC 8414: OAuth 2.0 Authorization Server Metadata
28
+ def authorization_server
29
+ supported_scopes = Mcp::Auth::ScopeRegistry.available_scopes.keys
30
+
31
+ metadata = {
32
+ issuer: authorization_server_url,
33
+ authorization_endpoint: "#{authorization_server_url}/oauth/authorize",
34
+ token_endpoint: "#{authorization_server_url}/oauth/token",
35
+ registration_endpoint: "#{authorization_server_url}/oauth/register",
36
+ revocation_endpoint: "#{authorization_server_url}/oauth/revoke",
37
+ introspection_endpoint: "#{authorization_server_url}/oauth/introspect",
38
+
39
+ # Supported features
40
+ scopes_supported: supported_scopes.uniq,
41
+ response_types_supported: %w[code],
42
+ grant_types_supported: %w[authorization_code refresh_token],
43
+ code_challenge_methods_supported: %w[S256], # PKCE required
44
+ token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none],
45
+
46
+ # RFC 8707: Resource Indicators
47
+ resource_parameter_supported: true,
48
+
49
+ # OAuth 2.1 features
50
+ authorization_response_iss_parameter_supported: true,
51
+ require_pushed_authorization_requests: false,
52
+ require_signed_request_object: false,
53
+
54
+ # Token revocation and introspection
55
+ revocation_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none],
56
+ introspection_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none]
57
+ }
58
+
59
+ render json: metadata, status: :ok, content_type: 'application/json'
60
+ end
61
+
62
+ # OpenID Connect Discovery
63
+ def openid_configuration
64
+ # Get all registered scopes plus openid, profile, email
65
+ supported_scopes = Mcp::Auth::ScopeRegistry.available_scopes.keys + %w[openid profile email]
66
+
67
+ metadata = {
68
+ issuer: authorization_server_url,
69
+ authorization_endpoint: "#{authorization_server_url}/oauth/authorize",
70
+ token_endpoint: "#{authorization_server_url}/oauth/token",
71
+ registration_endpoint: "#{authorization_server_url}/oauth/register",
72
+ jwks_uri: "#{authorization_server_url}/.well-known/jwks.json",
73
+ userinfo_endpoint: "#{authorization_server_url}/oauth/userinfo",
74
+
75
+ scopes_supported: supported_scopes.uniq,
76
+ response_types_supported: %w[code],
77
+ grant_types_supported: %w[authorization_code refresh_token],
78
+ code_challenge_methods_supported: %w[S256],
79
+ token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post none],
80
+ subject_types_supported: %w[public],
81
+ id_token_signing_alg_values_supported: %w[HS256]
82
+ }
83
+
84
+ render json: metadata, status: :ok, content_type: 'application/json'
85
+ end
86
+
87
+ # JWKS endpoint (empty for HMAC)
88
+ def jwks
89
+ keys = { keys: [] }
90
+ render json: keys, status: :ok, content_type: 'application/json'
91
+ end
92
+
93
+ private
94
+
95
+ def set_cors_headers
96
+ response.headers['Access-Control-Allow-Origin'] = '*'
97
+ response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
98
+ response.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
99
+ response.headers['Content-Type'] = 'application/json; charset=utf-8' unless request.method == 'OPTIONS'
100
+ end
101
+
102
+ def handle_options_request
103
+ head :no_content if request.method == 'OPTIONS'
104
+ end
105
+
106
+ def canonical_resource_url
107
+ # Use configured MCP server path
108
+ mcp_path = Mcp::Auth.configuration&.mcp_server_path || '/mcp'
109
+
110
+ # Ensure path starts with /
111
+ mcp_path = "/#{mcp_path}" unless mcp_path.start_with?('/')
112
+
113
+ # Remove trailing slash if present
114
+ mcp_path = mcp_path.chomp('/')
115
+
116
+ # Build the full resource URL
117
+ base_url = "#{request.scheme}://#{request.host_with_port}"
118
+ "#{base_url}#{mcp_path}"
119
+ end
120
+
121
+ def mcp_documentation_url
122
+ # Use configured docs URL, or generate default based on server path
123
+ docs_url = Mcp::Auth.configuration&.mcp_docs_url
124
+
125
+ if docs_url.present?
126
+ # If it's a full URL, use as-is
127
+ return docs_url if docs_url.start_with?('http://', 'https://')
128
+
129
+ # If it's a path, prepend base URL
130
+ return "#{request.base_url}#{docs_url}"
131
+ end
132
+
133
+ # Default: append /docs to the MCP server path
134
+ mcp_path = Mcp::Auth.configuration&.mcp_server_path || '/mcp'
135
+ mcp_path = "/#{mcp_path}" unless mcp_path.start_with?('/')
136
+ mcp_path = mcp_path.chomp('/')
137
+
138
+ "#{request.base_url}#{mcp_path}/docs"
139
+ end
140
+
141
+ def authorization_server_url
142
+ config_url = Mcp::Auth.configuration&.authorization_server_url
143
+ config_url.presence || "#{request.scheme}://#{request.host_with_port}"
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,30 @@
1
+ module Mcp
2
+ module Auth
3
+ class AccessToken < ActiveRecord::Base
4
+ self.table_name = "mcp_auth_access_tokens"
5
+
6
+ belongs_to :user
7
+ belongs_to :org, optional: true
8
+ belongs_to :oauth_client,
9
+ class_name: "Mcp::Auth::OauthClient",
10
+ foreign_key: :client_id,
11
+ primary_key: :client_id,
12
+ optional: true
13
+
14
+ validates :token, presence: true, uniqueness: true
15
+ validates :client_id, presence: true
16
+ validates :expires_at, presence: true
17
+
18
+ scope :active, -> { where('expires_at > ?', Time.current) }
19
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
20
+
21
+ def expired?
22
+ expires_at <= Time.current
23
+ end
24
+
25
+ def self.cleanup_expired
26
+ expired.delete_all
27
+ end
28
+ end
29
+ end
30
+ end