Class: McpAuthenticator
- Inherits:
-
Object
- Object
- McpAuthenticator
- Defined in:
- app/mcp/mcp_authenticator.rb
Overview
Handles authentication for MCP (Model Context Protocol) requests.
Supports two authentication methods:
-
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. -
ApiAuthentication bearer tokens (for Cursor/programmatic access)
Manual tokens created in CRM with per-token service scoping.
Security Requirements:
- Request must come from the MCP subdomain (mcp.warmlyyours.com)
- Client must send Bearer token in Authorization header
- Token must be valid and not expired/revoked
- 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
-
.authenticate(request) ⇒ AuthResult, ...
Check if a request is authenticated (doesn't raise).
-
.authenticate!(request) ⇒ AuthResult, OAuthAuthResult
Authenticate a request and return an AuthResult.
-
.current ⇒ AuthResult, ...
Auth result for the in-flight request.
-
.current=(auth_result) ⇒ Object
Set by the McpBearerAuth middleware (and cleared by it after each request).
-
.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.
Instance Method Summary collapse
- #authenticate ⇒ Object
- #authenticate! ⇒ Object
-
#initialize(request) ⇒ McpAuthenticator
constructor
A new instance of McpAuthenticator.
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)
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
151 152 153 |
# File 'app/mcp/mcp_authenticator.rb', line 151 def self.authenticate!(request) new(request).authenticate! end |
.current ⇒ AuthResult, ...
Returns auth result for the in-flight request.
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.
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.
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
#authenticate ⇒ Object
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 |