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: http://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
45
46
47
48
# 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
  @extra_client_metadata = 
  @http_client = create_http_client
  # Protected resource metadata learned from a 401 WWW-Authenticate
  # challenge, reused by discovery so a challenge-advertised metadata URL
  # is not re-derived (and possibly missed).
  @challenge_resource_metadata = nil
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



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/mcp_client/auth/oauth_provider.rb', line 57

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



133
134
135
136
137
138
139
140
# File 'lib/mcp_client/auth/oauth_provider.rb', line 133

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:



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

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

#extract_resource_metadata_url(header) ⇒ String?

Extract the protected-resource-metadata URL from a WWW-Authenticate header. Per RFC 9728 the parameter is resource_metadata; a legacy resource parameter is accepted as a fallback for older servers.

Parameters:

  • header (String)

    the WWW-Authenticate header value

Returns:

  • (String, nil)

    the metadata URL if present



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/mcp_client/auth/oauth_provider.rb', line 166

def (header)
  # Auth-params may include optional whitespace around '=' (RFC 7235).
  # Quoted form: resource_metadata = "https://..."
  if (m = header.match(/resource_metadata\s*=\s*"([^"]+)"/))
    return m[1]
  end

  # Unquoted token form: resource_metadata = https://...
  if (m = header.match(/resource_metadata\s*=\s*([^,\s]+)/))
    return m[1]
  end

  # Legacy fallback: resource="https://..."
  header.match(/resource\s*=\s*"([^"]+)"/)&.captures&.first
end

#handle_unauthorized_response(response) ⇒ ResourceMetadata?

Handle 401 Unauthorized response (for server discovery)

Parameters:

  • response (Faraday::Response)

    HTTP response

Returns:



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/mcp_client/auth/oauth_provider.rb', line 145

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

  url = (www_authenticate)
  return nil unless url

  # This URL was explicitly advertised by the 401 challenge, so a 404 is a
  # misconfiguration to surface (strict), not a speculative miss to skip.
   = (url, strict: true)
  # Reuse this challenge-advertised metadata during the subsequent OAuth
  # flow instead of re-deriving (and possibly missing) the well-known URL.
  @challenge_resource_metadata = 
  
end

#start_authorization_flowString

Start OAuth authorization flow

Returns:

  • (String)

    Authorization URL to redirect user to

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/mcp_client/auth/oauth_provider.rb', line 80

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:



73
74
75
# File 'lib/mcp_client/auth/oauth_provider.rb', line 73

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