Class: McpAuthenticator

Inherits:
Object
  • Object
show all
Defined in:
app/mcp/mcp_authenticator.rb

Overview

Handles authentication for MCP (Model Context Protocol) requests.
Supports two authentication methods:

  1. Doorkeeper OAuth tokens (preferred for Claude Desktop/native clients)
    Obtained via the OAuth 2.1 authorization code flow with PKCE.
    Service access is scoped by the OAuth application's permitted_services.
    If permitted_services is empty, grants access to all services.

  2. ApiAuthentication bearer tokens (for Cursor/programmatic access)
    Manual tokens created in CRM with per-token service scoping.

Security Requirements:

  1. Request must come from the MCP subdomain (mcp.warmlyyours.com)
  2. Client must send Bearer token in Authorization header
  3. Token must be valid and not expired/revoked
  4. Account must be an employee with 'mcp_access' role (admin has it by default)

Returns an AuthResult (or OAuthAuthResult) containing the account and auth info.

Defined Under Namespace

Modules: ScopedServices Classes: AuthResult, AuthenticationError, AuthorizationError, OAuthAuthResult

Constant Summary collapse

REQUIRED_ROLE =

Required role.

'mcp_access'
ALLOWED_SUBDOMAINS =

Allowed subdomains.

%w[mcp].freeze
ENV_KEY =

Rack env key for storing auth result

'mcp.auth_result'
THREAD_KEY =

Thread-local key for the in-flight request's auth result. Set by the
McpBearerAuth middleware: the MCP SDK doesn't thread the Rack env through to
tool calls, so handlers and tools read the result from here.

:mcp_auth_result

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request) ⇒ McpAuthenticator

Returns a new instance of McpAuthenticator.



162
163
164
# File 'app/mcp/mcp_authenticator.rb', line 162

def initialize(request)
  @request = request
end

Class Method Details

.authenticate(request) ⇒ AuthResult, ...

Check if a request is authenticated (doesn't raise)

Parameters:

  • request (Rack::Request)

    The incoming request

Returns:



158
159
160
# File 'app/mcp/mcp_authenticator.rb', line 158

def self.authenticate(request)
  new(request).authenticate
end

.authenticate!(request) ⇒ AuthResult, OAuthAuthResult

Authenticate a request and return an AuthResult

Parameters:

  • request (Rack::Request)

    The incoming request

Returns:

Raises:



151
152
153
# File 'app/mcp/mcp_authenticator.rb', line 151

def self.authenticate!(request)
  new(request).authenticate!
end

.currentAuthResult, ...

Returns auth result for the in-flight request.

Returns:



36
37
38
# File 'app/mcp/mcp_authenticator.rb', line 36

def self.current
  Thread.current[THREAD_KEY]
end

.current=(auth_result) ⇒ Object

Set by the McpBearerAuth middleware (and cleared by it after each request).
Paired with .current so THREAD_KEY has exactly one reader and one writer —
nothing should reach for the bare thread-local.

Parameters:



45
46
47
# File 'app/mcp/mcp_authenticator.rb', line 45

def self.current=(auth_result)
  Thread.current[THREAD_KEY] = auth_result
end

.current_can_access_any?(service_keys) ⇒ Boolean

Single rule for "may this request reach this tool", shared by the tools/list
visibility filter and the tools/call execution guard so the two can't drift.
Fails closed: no auth, or a tool with no declared service, is denied.

Parameters:

  • service_keys (String, Array<String>, nil)

    any-of; the tool is allowed
    when the caller holds at least one.

Returns:

  • (Boolean)


56
57
58
59
60
61
# File 'app/mcp/mcp_authenticator.rb', line 56

def self.current_can_access_any?(service_keys)
  auth = current
  return false if auth.nil?

  Array(service_keys).any? { |key| auth.can_access_service?(key) }
end

Instance Method Details

#authenticateObject



173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'app/mcp/mcp_authenticator.rb', line 173

def authenticate
  # Verify request is from allowed subdomain
  return nil unless valid_subdomain?

  token_string = extract_bearer_token
  return nil if token_string.blank?

  # Try Doorkeeper OAuth token first
  result = authenticate_doorkeeper(token_string)
  return result if result

  # Fall back to ApiAuthentication bearer token
  authenticate_api_token(token_string)
end

#authenticate!Object



166
167
168
169
170
171
# File 'app/mcp/mcp_authenticator.rb', line 166

def authenticate!
  result = authenticate
  raise AuthenticationError, 'Invalid or missing authentication token' unless result

  result
end