Class: Mcp::Auth::OauthController

Inherits:
ApplicationController
  • Object
show all
Defined in:
app/controllers/mcp/auth/oauth_controller.rb

Instance Method Summary collapse

Instance Method Details

#approveObject

Consent approval endpoint



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 27

def approve
  return redirect_to main_app.new_user_session_path unless mcp_user_signed_in?

  return render_error('invalid_request', 'Missing required parameters') unless valid_authorization_params?

  if params[:approved] == 'true'
    # Get selected scopes from checkboxes
    selected_scopes = Array(params[:scopes]).compact.reject(&:blank?)

    Rails.logger.info "[OAuth] User selected scopes: #{selected_scopes.inspect}"

    # Validate selected scopes
    if selected_scopes.blank?
      Rails.logger.warn '[OAuth] No scopes selected'
      return render_error('invalid_request', 'At least one scope must be selected')
    end

    # Get originally requested scopes
    requested_scopes = params[:scope]&.split || []

    # Get required scopes from the requested list
    required_scopes = get_required_scopes(requested_scopes)

    # Check all required scopes are selected
    missing_required = required_scopes - selected_scopes
    if missing_required.any?
      Rails.logger.warn "[OAuth] Missing required scopes: #{missing_required.join(', ')}"
      return render_error('invalid_request', 'Required scopes must be selected')
    end
    approved_scopes = Mcp::Auth::ScopeRegistry.validate_scopes(selected_scopes)

    # Preserve standard OpenID Connect scopes that were originally requested.
    # They gate identity claims (already governed by the userinfo/id_token
    # endpoints) rather than application resources, so they are not rendered
    # as individual consent checkboxes but must survive the approval step.
    oidc_scopes = requested_scopes & Mcp::Auth::ScopeRegistry::STANDARD_OIDC_SCOPES
    approved_scope_string = (approved_scopes + oidc_scopes).uniq.join(' ')

    # Generate authorization code with ONLY approved scopes
    generate_and_redirect_with_code(approved_scope_string)
  else
    redirect_with_error('access_denied', 'User denied the request')
  end
end

#authorizeObject

OAuth 2.1 Authorization endpoint (GET/POST)



12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 12

def authorize
  Rails.logger.info "[OAuth] Authorization request for client=#{params[:client_id]} scope=#{params[:scope]}"

  unless valid_authorization_params?
    return render_error('invalid_request', 'Missing or invalid required parameters')
  end

  if mcp_user_signed_in?
    handle_signed_in_user
  else
    
  end
end

#introspectObject

RFC 7662: Token Introspection Requires client authentication. Tokens not owned by the authenticated client are reported as ‘false` to prevent token-scanning.



125
126
127
128
129
130
131
132
133
134
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 125

def introspect
  client = authenticate_client
  return render_error('invalid_client', 'Client authentication failed', status: :unauthorized) unless client

  token = params[:token]
  return render json: { active: false }, content_type: 'application/json' if token.blank?

  response = introspect_token_for_client(token, client)
  render json: response, content_type: 'application/json'
end

#registerObject

RFC 7591: Dynamic Client Registration



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 85

def register
  Rails.logger.info '[OAuth] Client registration request'

  begin
    client_data = build_client_registration
    oauth_client = Mcp::Auth::OauthClient.create!(client_data)

    Rails.logger.info "[OAuth] Client registered: #{oauth_client.client_id}"
    render json: format_client_response(oauth_client), content_type: 'application/json'
  rescue ActiveRecord::RecordInvalid => e
    render_error('invalid_client_metadata', e.message)
  rescue ArgumentError => e
    render_error('invalid_request', e.message)
  rescue StandardError => e
    Rails.logger.error "[OAuth] Registration error: #{e.message}"
    render_error('server_error', 'An unexpected error occurred')
  end
end

#revokeObject

RFC 7009: Token Revocation Requires client authentication; only revokes tokens that belong to the authenticated client (otherwise still returns 200 to avoid leaking which tokens exist — per RFC 7009 §2.2).



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 108

def revoke
  client = authenticate_client
  return render_error('invalid_client', 'Client authentication failed', status: :unauthorized) unless client

  token = params[:token]
  return render_error('invalid_request', 'Token parameter is required') if token.blank?

  revoked = revoke_token_for_client(token, client, hint: params[:token_type_hint])

  # RFC 7009: Always return 200 OK regardless of whether the token was found.
  Rails.logger.info "[OAuth] Token revocation by client=#{client.client_id}: #{revoked ? 'success' : 'not found / not owned'}"
  head :ok
end

#tokenObject

OAuth 2.1 Token endpoint



73
74
75
76
77
78
79
80
81
82
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 73

def token
  case params[:grant_type]
  when 'authorization_code'
    handle_authorization_code_grant
  when 'refresh_token'
    handle_refresh_token_grant
  else
    render_error('unsupported_grant_type', 'Grant type not supported')
  end
end

#userinfoObject

OpenID Connect UserInfo Endpoint



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/controllers/mcp/auth/oauth_controller.rb', line 137

def userinfo
  auth_header = request.headers['Authorization']

  return render json: { error: 'invalid_token' }, status: :unauthorized unless auth_header&.start_with?('Bearer ')

  token = auth_header.split(' ', 2).last
  payload = Services::TokenService.validate_access_token(token)

  return render json: { error: 'invalid_token' }, status: :unauthorized unless payload

   = {
    sub: payload[:sub],
    email: payload[:email],
    email_verified: true,
    name: payload[:email],
    preferred_username: payload[:email]
  }
  [:org] = payload[:org] if payload[:org]

  render json: , content_type: 'application/json'
end