Class: MCPClient::Auth::BrowserOAuth
- Inherits:
-
Object
- Object
- MCPClient::Auth::BrowserOAuth
- Defined in:
- lib/mcp_client/auth/browser_oauth.rb
Overview
Browser-based OAuth authentication flow helper Provides a complete OAuth flow using browser authentication with a local callback server
Defined Under Namespace
Classes: CallbackServer
Instance Attribute Summary collapse
-
#callback_path ⇒ String
readonly
Path for OAuth callback.
-
#callback_port ⇒ Integer
readonly
Port for local callback server.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#oauth_provider ⇒ OAuthProvider
readonly
The OAuth provider instance.
Instance Method Summary collapse
-
#authenticate(timeout: 300, auto_open_browser: true) ⇒ Token
Perform complete browser-based OAuth authentication flow This will: 1.
-
#handle_http_request(client, result, mutex, condition) ⇒ Object
Handle HTTP request from OAuth callback.
-
#initialize(oauth_provider, callback_port: 8080, callback_path: '/callback', logger: nil) ⇒ BrowserOAuth
constructor
Initialize browser OAuth helper.
-
#open_browser(url) ⇒ Boolean
Open URL in default browser.
-
#parse_query_params(query_string) ⇒ Hash
Parse URL query parameters.
-
#send_http_response(client, status_code, content_type, body) ⇒ Object
Send HTTP response to client.
-
#start_callback_server(result, mutex, condition) ⇒ CallbackServer
Start the local callback server using TCPServer.
Constructor Details
#initialize(oauth_provider, callback_port: 8080, callback_path: '/callback', logger: nil) ⇒ BrowserOAuth
Initialize browser OAuth helper
28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 28 def initialize(oauth_provider, callback_port: 8080, callback_path: '/callback', logger: nil) @oauth_provider = oauth_provider @callback_port = callback_port @callback_path = callback_path @logger = logger || Logger.new($stdout, level: Logger::WARN) # Ensure OAuth provider's redirect_uri matches our callback server expected_redirect_uri = "http://localhost:#{callback_port}#{callback_path}" return unless oauth_provider.redirect_uri != expected_redirect_uri @logger.warn("OAuth provider redirect_uri (#{oauth_provider.redirect_uri}) doesn't match " \ "callback server (#{expected_redirect_uri}). Updating redirect_uri.") oauth_provider.redirect_uri = expected_redirect_uri end |
Instance Attribute Details
#callback_path ⇒ String (readonly)
Returns Path for OAuth callback.
21 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 21 attr_reader :oauth_provider, :callback_port, :callback_path, :logger |
#callback_port ⇒ Integer (readonly)
Returns Port for local callback server.
21 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 21 attr_reader :oauth_provider, :callback_port, :callback_path, :logger |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
21 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 21 attr_reader :oauth_provider, :callback_port, :callback_path, :logger |
#oauth_provider ⇒ OAuthProvider (readonly)
Returns The OAuth provider instance.
21 22 23 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 21 def oauth_provider @oauth_provider end |
Instance Method Details
#authenticate(timeout: 300, auto_open_browser: true) ⇒ Token
Perform complete browser-based OAuth authentication flow This will:
-
Start a local HTTP server to handle the callback
-
Open the authorization URL in the user’s browser
-
Wait for the user to authorize and receive the callback
-
Complete the OAuth flow and return the token
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 54 def authenticate(timeout: 300, auto_open_browser: true) # Start authorization flow and get URL auth_url = @oauth_provider. @logger.debug("Authorization URL: #{auth_url}") # Create a result container to share data between threads result = { code: nil, state: nil, error: nil, completed: false } mutex = Mutex.new condition = ConditionVariable.new # Start local callback server server = start_callback_server(result, mutex, condition) begin # Open browser to authorization URL if auto_open_browser open_browser(auth_url) @logger.info("\nOpening browser for authorization...") @logger.info("If browser doesn't open automatically, visit this URL:") else @logger.info("\nPlease visit this URL to authorize:") end @logger.info(auth_url) @logger.info("\nWaiting for authorization...") # Wait for callback with timeout mutex.synchronize do condition.wait(mutex, timeout) unless result[:completed] end # Check if we got a response raise Timeout::Error, "OAuth authorization timed out after #{timeout} seconds" unless result[:completed] # Check for errors raise MCPClient::Errors::ConnectionError, "OAuth authorization failed: #{result[:error]}" if result[:error] # Complete OAuth flow @logger.debug('Completing OAuth authorization flow') token = @oauth_provider.(result[:code], result[:state]) @logger.info("\nAuthentication successful!") token ensure # Always shutdown the server server&.shutdown end end |
#handle_http_request(client, result, mutex, condition) ⇒ Object
Handle HTTP request from OAuth callback
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 152 def handle_http_request(client, result, mutex, condition) # Set read timeout to prevent hanging connections client.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [5, 0].pack('l_2')) # Read request line request_line = client.gets return unless request_line parts = request_line.split return unless parts.length >= 2 method, path = parts[0..1] @logger.debug("Received #{method} request: #{path}") # Read and discard headers until blank line (with limit to prevent memory exhaustion) header_count = 0 loop do break if header_count >= 100 # Limit header count line = client.gets break if line.nil? || line.strip.empty? header_count += 1 end # Parse path and query parameters uri_path, query_string = path.split('?', 2) # Only handle our callback path unless uri_path == @callback_path send_http_response(client, 404, 'text/plain', 'Not Found') return end # Parse query parameters params = parse_query_params(query_string || '') @logger.debug("Callback params: #{params.keys.join(', ')}") # Extract OAuth parameters code = params['code'] state = params['state'] error = params['error'] error_description = params['error_description'] # Update result and signal waiting thread mutex.synchronize do if error result[:error] = error_description || error elsif code && state result[:code] = code result[:state] = state else result[:error] = 'Invalid callback: missing code or state parameter' end result[:completed] = true condition.signal end # Send HTML response to browser if result[:error] send_http_response(client, 400, 'text/html', error_page(result[:error])) else send_http_response(client, 200, 'text/html', success_page) end ensure client&.close end |
#open_browser(url) ⇒ Boolean
Open URL in default browser
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 263 def open_browser(url) case RbConfig::CONFIG['host_os'] when /darwin/ system('open', url) when /linux|bsd/ system('xdg-open', url) when /mswin|mingw|cygwin/ system('start', url) else @logger.warn('Unknown operating system, cannot open browser automatically') false end rescue StandardError => e @logger.warn("Failed to open browser: #{e.}") false end |
#parse_query_params(query_string) ⇒ Hash
Parse URL query parameters
225 226 227 228 229 230 231 232 233 234 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 225 def parse_query_params(query_string) params = {} query_string.split('&').each do |param| next if param.empty? key, value = param.split('=', 2) params[CGI.unescape(key)] = CGI.unescape(value || '') end params end |
#send_http_response(client, status_code, content_type, body) ⇒ Object
Send HTTP response to client
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 242 def send_http_response(client, status_code, content_type, body) status_text = case status_code when 200 then 'OK' when 400 then 'Bad Request' when 404 then 'Not Found' else 'Unknown' end response = "HTTP/1.1 #{status_code} #{status_text}\r\n" response += "Content-Type: #{content_type}; charset=utf-8\r\n" response += "Content-Length: #{body.bytesize}\r\n" response += "Connection: close\r\n" response += "\r\n" response += body client.print(response) end |
#start_callback_server(result, mutex, condition) ⇒ CallbackServer
Start the local callback server using TCPServer
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/mcp_client/auth/browser_oauth.rb', line 109 def start_callback_server(result, mutex, condition) begin server = TCPServer.new('127.0.0.1', @callback_port) @logger.debug("Started callback server on http://127.0.0.1:#{@callback_port}#{@callback_path}") rescue Errno::EADDRINUSE raise MCPClient::Errors::ConnectionError, "Cannot start OAuth callback server: port #{@callback_port} is already in use. " \ 'Please close the application using this port or choose a different callback_port.' rescue StandardError => e raise MCPClient::Errors::ConnectionError, "Failed to start OAuth callback server on port #{@callback_port}: #{e.}" end running = true # Start server in background thread thread = Thread.new do while running begin # Use wait_readable with timeout to allow checking the running flag next unless server.wait_readable(0.5) client = server.accept handle_http_request(client, result, mutex, condition) rescue IOError, Errno::EBADF # Server was closed, exit loop break rescue StandardError => e @logger.error("Error handling callback request: #{e.}") end end end # Return an object with shutdown method for compatibility CallbackServer.new(server, thread, -> { running = false }) end |