Class: RubyLLM::MCP::Auth::BrowserOAuthProvider

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_llm/mcp/auth/browser_oauth_provider.rb

Overview

Browser-based OAuth authentication provider Provides complete OAuth 2.1 flow with automatic browser opening and local callback server Compatible API with OAuthProvider for seamless interchange

Defined Under Namespace

Classes: NullLogger, SynchronizedLogger

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server_url: nil, oauth_provider: nil, callback_port: 8080, callback_path: "/callback", logger: nil, storage: nil, redirect_uri: nil, scope: nil) ⇒ BrowserOAuthProvider

Returns a new instance of BrowserOAuthProvider.

Parameters:

  • server_url (String) (defaults to: nil)

    OAuth server URL (alternative to oauth_provider)

  • oauth_provider (OAuthProvider) (defaults to: nil)

    OAuth provider instance (alternative to server_url)

  • callback_port (Integer) (defaults to: 8080)

    port for local callback server

  • callback_path (String) (defaults to: "/callback")

    path for callback URL

  • logger (Logger) (defaults to: nil)

    logger instance

  • storage (Object) (defaults to: nil)

    token storage instance

  • redirect_uri (String) (defaults to: nil)

    OAuth redirect URI

  • scope (String) (defaults to: nil)

    OAuth scopes



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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 74

def initialize(server_url: nil, oauth_provider: nil, callback_port: 8080, callback_path: "/callback", # rubocop:disable Metrics/ParameterLists
               logger: nil, storage: nil, redirect_uri: nil, scope: nil)
  @logger = logger || MCP.logger
  @synchronized_logger = SynchronizedLogger.new(@logger)
  @callback_logger = NullLogger.new
  @callback_port = callback_port
  @callback_path = callback_path

  # Set redirect_uri before creating oauth_provider
  redirect_uri ||= "http://localhost:#{callback_port}#{callback_path}"

  # Either accept an existing oauth_provider or create one
  if oauth_provider
    @oauth_provider = oauth_provider
    # Sync attributes from the provided oauth_provider
    @server_url = oauth_provider.server_url
    @redirect_uri = oauth_provider.redirect_uri
    @scope = oauth_provider.scope
    @storage = oauth_provider.storage
  elsif server_url
    @server_url = server_url
    @redirect_uri = redirect_uri
    @scope = scope
    @storage = storage || MemoryStorage.new
    # Create a new oauth_provider
    @oauth_provider = OAuthProvider.new(
      server_url: server_url,
      redirect_uri: redirect_uri,
      scope: scope,
      logger: @synchronized_logger,
      storage: @storage
    )
  else
    raise ArgumentError, "Either server_url or oauth_provider must be provided"
  end

  # Ensure OAuth provider redirect_uri matches our callback server
  validate_and_sync_redirect_uri!

  # Initialize browser helpers
  @http_server = Browser::HttpServer.new(port: @callback_port, logger: @callback_logger)
  @callback_handler = Browser::CallbackHandler.new(callback_path: @callback_path, logger: @callback_logger)
  @pages = Browser::Pages.new(
    custom_success_page: MCP.config.oauth.browser_success_page,
    custom_error_page: MCP.config.oauth.browser_error_page
  )
  @opener = Browser::Opener.new(logger: @synchronized_logger)
end

Instance Attribute Details

#callback_pathObject (readonly)

Returns the value of attribute callback_path.



54
55
56
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 54

def callback_path
  @callback_path
end

#callback_portObject (readonly)

Returns the value of attribute callback_port.



54
55
56
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 54

def callback_port
  @callback_port
end

#loggerObject (readonly)

Returns the value of attribute logger.



54
55
56
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 54

def logger
  @logger
end

#oauth_providerObject (readonly)

Returns the value of attribute oauth_provider.



54
55
56
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 54

def oauth_provider
  @oauth_provider
end

#redirect_uriObject

Returns the value of attribute redirect_uri.



55
56
57
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 55

def redirect_uri
  @redirect_uri
end

#scopeObject

Returns the value of attribute scope.



55
56
57
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 55

def scope
  @scope
end

#server_urlObject

Returns the value of attribute server_url.



55
56
57
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 55

def server_url
  @server_url
end

#storageObject

Returns the value of attribute storage.



55
56
57
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 55

def storage
  @storage
end

Instance Method Details

#access_tokenToken?

Get current access token (for compatibility with OAuthProvider)

Returns:

  • (Token, nil)

    valid access token or nil



182
183
184
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 182

def access_token
  @oauth_provider.access_token
end

#apply_authorization(request) ⇒ Object

Apply authorization header to HTTP request (for compatibility with OAuthProvider)

Parameters:

  • request (HTTPX::Request)

    HTTP request object



188
189
190
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 188

def apply_authorization(request)
  @oauth_provider.apply_authorization(request)
end

#authenticate(timeout: 300, auto_open_browser: true) ⇒ Token

Perform complete OAuth authentication flow with browser Compatible with OAuthProvider's authentication pattern

Parameters:

  • timeout (Integer) (defaults to: 300)

    seconds to wait for authorization

  • auto_open_browser (Boolean) (defaults to: true)

    automatically open browser

Returns:

  • (Token)

    access token



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 128

def authenticate(timeout: 300, auto_open_browser: true)
  # 1. Start authorization flow and get URL
  auth_url = @oauth_provider.start_authorization_flow
  @synchronized_logger.debug("Authorization URL: #{auth_url}")

  # 2. Create result container for thread coordination
  result = { code: nil, state: nil, error: nil, completed: false }
  mutex = Mutex.new
  condition = ConditionVariable.new

  # 3. Start local callback server
  server = start_callback_server(result, mutex, condition)

  begin
    announce_authorization_flow(auth_url, auto_open_browser)

    # Allow callback worker to begin processing only after setup logging/browser open
    # to reduce cross-thread test-double races under JRuby.
    server.start

    # 5. Wait for callback with timeout
    mutex.synchronize do
      condition.wait(mutex, timeout) unless result[:completed]
    end

    snapshot = mutex.synchronize { result.dup }

    unless snapshot[:completed]
      raise Errors::TimeoutError.new(message: "OAuth authorization timed out after #{timeout} seconds")
    end

    if snapshot[:error]
      raise Errors::TransportError.new(message: "OAuth authorization failed: #{snapshot[:error]}")
    end

    # Stop callback server before token exchange to avoid cross-thread races
    # (observed under JRuby when background callback logging overlaps main-thread mocks).
    server&.shutdown
    server = nil

    # 6. Complete OAuth flow
    @synchronized_logger.debug("Completing OAuth authorization flow")
    token = @oauth_provider.complete_authorization_flow(snapshot[:code], snapshot[:state])

    @synchronized_logger.info("\nAuthentication successful!")
    token
  ensure
    # Always shutdown the server
    server&.shutdown
  end
end

#complete_authorization_flow(code, state) ⇒ Token

Complete authorization flow (for compatibility with OAuthProvider)

Parameters:

  • code (String)

    authorization code

  • state (String)

    state parameter

Returns:

  • (Token)

    access token



202
203
204
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 202

def complete_authorization_flow(code, state)
  @oauth_provider.complete_authorization_flow(code, state)
end

#custom_error_pageObject



62
63
64
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 62

def custom_error_page
  @pages.instance_variable_get(:@custom_error_page)
end

#custom_success_pageObject

Expose custom pages for testing/inspection



58
59
60
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 58

def custom_success_page
  @pages.instance_variable_get(:@custom_success_page)
end

#handle_authentication_challenge(www_authenticate: nil, resource_metadata: nil, resource_metadata_url: nil, requested_scope: nil) ⇒ Boolean

Handle authentication challenge with browser-based auth

Parameters:

  • www_authenticate (String, nil) (defaults to: nil)

    WWW-Authenticate header value

  • resource_metadata (String, nil) (defaults to: nil)

    Resource metadata URL from response/challenge

  • resource_metadata_url (String, nil) (defaults to: nil)

    Legacy alias for resource_metadata

  • requested_scope (String, nil) (defaults to: nil)

    Scope from WWW-Authenticate challenge

Returns:

  • (Boolean)

    true if authentication was completed successfully



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 212

def handle_authentication_challenge(www_authenticate: nil, resource_metadata: nil, resource_metadata_url: nil,
                                    requested_scope: nil)
  @synchronized_logger.debug("BrowserOAuthProvider handling authentication challenge")

  # Try standard provider's automatic handling first (token refresh, client credentials)
  begin
    return @oauth_provider.handle_authentication_challenge(
      www_authenticate: www_authenticate,
      resource_metadata: ,
      resource_metadata_url: ,
      requested_scope: requested_scope
    )
  rescue Errors::AuthenticationRequiredError
    # Standard provider couldn't handle it - need interactive auth
    @synchronized_logger.info("Automatic authentication failed, starting browser-based OAuth flow")
  end

  # Perform full browser-based authentication
  authenticate(auto_open_browser: true)
  true
end

#parse_www_authenticate(header) ⇒ Hash

Parse WWW-Authenticate header (delegate to oauth_provider)

Parameters:

  • header (String)

    WWW-Authenticate header value

Returns:

  • (Hash)

    parsed challenge information



237
238
239
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 237

def parse_www_authenticate(header)
  @oauth_provider.parse_www_authenticate(header)
end

#start_authorization_flowString

Start authorization flow (for compatibility with OAuthProvider)

Returns:

  • (String)

    authorization URL



194
195
196
# File 'lib/ruby_llm/mcp/auth/browser_oauth_provider.rb', line 194

def start_authorization_flow
  @oauth_provider.start_authorization_flow
end