Class: MCPClient::Auth::OAuthProvider

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_client/auth/oauth_provider.rb

Overview

OAuth 2.1 provider for MCP client authentication Handles the complete OAuth flow including server discovery, client registration, authorization, token exchange, and refresh

Defined Under Namespace

Classes: MemoryStorage

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(server_url:, redirect_uri: 'http://localhost:8080/callback', scope: nil, logger: nil, storage: nil, client_metadata: {}) ⇒ OAuthProvider

Initialize OAuth provider

Parameters:

  • server_url (String)

    The MCP server URL (used as OAuth resource parameter)

  • redirect_uri (String) (defaults to: 'http://localhost:8080/callback')

    OAuth redirect URI (default: localhost:8080/callback)

  • scope (String, Symbol, nil) (defaults to: nil)

    OAuth scope (use :all for all server-supported scopes)

  • logger (Logger, nil) (defaults to: nil)

    Optional logger

  • storage (Object, nil) (defaults to: nil)

    Storage backend for tokens and client info

  • client_metadata (Hash) (defaults to: {})

    Extra OIDC client metadata fields for DCR registration. Supported keys: :client_name, :client_uri, :logo_uri, :tos_uri, :policy_uri, :contacts



35
36
37
38
39
40
41
42
43
44
# File 'lib/mcp_client/auth/oauth_provider.rb', line 35

def initialize(server_url:, redirect_uri: 'http://localhost:8080/callback', scope: nil, logger: nil, storage: nil,
               client_metadata: {})
  self.server_url = server_url
  self.redirect_uri = redirect_uri
  self.scope = scope
  self.logger = logger || Logger.new($stdout, level: Logger::WARN)
  self.storage = storage || MemoryStorage.new
   = 
  @http_client = create_http_client
end

Instance Attribute Details

#loggerLogger

Returns Logger instance.

Returns:

  • (Logger)

    Logger instance



24
# File 'lib/mcp_client/auth/oauth_provider.rb', line 24

attr_accessor :redirect_uri, :scope, :logger, :storage

#redirect_uriString

Returns OAuth redirect URI.

Returns:

  • (String)

    OAuth redirect URI



24
25
26
# File 'lib/mcp_client/auth/oauth_provider.rb', line 24

def redirect_uri
  @redirect_uri
end

#scopeString, ...

Returns OAuth scope (use :all for all server-supported scopes).

Returns:

  • (String, Symbol, nil)

    OAuth scope (use :all for all server-supported scopes)



24
# File 'lib/mcp_client/auth/oauth_provider.rb', line 24

attr_accessor :redirect_uri, :scope, :logger, :storage

#server_urlString

Returns The MCP server URL (normalized).

Returns:

  • (String)

    The MCP server URL (normalized)



24
# File 'lib/mcp_client/auth/oauth_provider.rb', line 24

attr_accessor :redirect_uri, :scope, :logger, :storage

#storageObject

Returns Storage backend for tokens and client info.

Returns:

  • (Object)

    Storage backend for tokens and client info



24
# File 'lib/mcp_client/auth/oauth_provider.rb', line 24

attr_accessor :redirect_uri, :scope, :logger, :storage

Instance Method Details

#access_tokenToken?

Get current access token (refresh if needed)

Returns:

  • (Token, nil)

    Current valid access token or nil



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/mcp_client/auth/oauth_provider.rb', line 53

def access_token
  token = storage.get_token(server_url)
  logger.debug("OAuth access_token: retrieved token=#{token ? 'present' : 'nil'} for #{server_url}")
  return nil unless token

  # Return token if still valid
  return token unless token.expired? || token.expires_soon?

  # Try to refresh if we have a refresh token
  refresh_token(token) if token.refresh_token
end

#apply_authorization(request) ⇒ void

This method returns an undefined value.

Apply OAuth authorization to HTTP request

Parameters:

  • request (Faraday::Request)

    HTTP request to authorize



129
130
131
132
133
134
135
136
# File 'lib/mcp_client/auth/oauth_provider.rb', line 129

def apply_authorization(request)
  token = access_token
  logger.debug("OAuth apply_authorization: token=#{token ? 'present' : 'nil'}")
  return unless token

  logger.debug("OAuth applying authorization header: #{token.to_header[0..20]}...")
  request.headers['Authorization'] = token.to_header
end

#complete_authorization_flow(code, state) ⇒ Token

Complete OAuth authorization flow with authorization code

Parameters:

  • code (String)

    Authorization code from callback

  • state (String)

    State parameter from callback

Returns:

  • (Token)

    Access token

Raises:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/mcp_client/auth/oauth_provider.rb', line 101

def complete_authorization_flow(code, state)
  # Verify state parameter
  stored_state = storage.get_state(server_url)
  raise ArgumentError, 'Invalid state parameter' unless stored_state == state

  # Get stored PKCE and client info
  pkce = storage.get_pkce(server_url)
  client_info = storage.get_client_info(server_url)
   = discover_authorization_server

  raise MCPClient::Errors::ConnectionError, 'Missing PKCE or client info' unless pkce && client_info

  # Exchange authorization code for tokens
  token = exchange_authorization_code(, client_info, code, pkce)

  # Store token
  storage.set_token(server_url, token)

  # Clean up temporary data
  storage.delete_pkce(server_url)
  storage.delete_state(server_url)

  token
end

#handle_unauthorized_response(response) ⇒ ResourceMetadata?

Handle 401 Unauthorized response (for server discovery)

Parameters:

  • response (Faraday::Response)

    HTTP response

Returns:



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/mcp_client/auth/oauth_provider.rb', line 141

def handle_unauthorized_response(response)
  www_authenticate = response.headers['WWW-Authenticate'] || response.headers['www-authenticate']
  return nil unless www_authenticate

  # Parse WWW-Authenticate header to extract resource metadata URL
  # Format: Bearer resource="https://example.com/.well-known/oauth-protected-resource"
  if (match = www_authenticate.match(/resource="([^"]+)"/))
     = match[1]
    ()
  end
end

#start_authorization_flowString

Start OAuth authorization flow

Returns:

  • (String)

    Authorization URL to redirect user to

Raises:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/mcp_client/auth/oauth_provider.rb', line 76

def start_authorization_flow
  # Discover authorization server
   = discover_authorization_server

  # Register client if needed
  client_info = get_or_register_client()

  # Generate PKCE parameters
  pkce = PKCE.new
  storage.set_pkce(server_url, pkce)

  # Generate state parameter
  state = SecureRandom.urlsafe_base64(32)
  storage.set_state(server_url, state)

  # Build authorization URL
  build_authorization_url(, client_info, pkce, state)
end

#supported_scopesArray<String>

Return the scopes supported by the authorization server Discovers server metadata and returns the scopes_supported list.

Returns:

  • (Array<String>)

    supported scopes, or empty array if not advertised

Raises:



69
70
71
# File 'lib/mcp_client/auth/oauth_provider.rb', line 69

def supported_scopes
  @supported_scopes ||= discover_authorization_server.scopes_supported || []
end