Class: MCP::Client::OAuth::Flow
- Inherits:
-
Object
- Object
- MCP::Client::OAuth::Flow
- Defined in:
- lib/mcp/client/oauth/flow.rb
Overview
Internal orchestrator for the MCP OAuth 2.1 + PKCE + DCR authorization flow. Driven by ‘MCP::Client::HTTP` on a 401 response. The user-facing surface is `Provider`; this class consumes a Provider plus signal data extracted from the failing response (resource_metadata URL, scope challenge).
Defined Under Namespace
Classes: AuthorizationError, InvalidGrantError
Instance Method Summary collapse
-
#initialize(provider:, http_client_factory: nil) ⇒ Flow
constructor
A new instance of Flow.
-
#refresh!(server_url:, resource_metadata_url: nil) ⇒ Object
Exchanges the saved ‘refresh_token` for a fresh access token (RFC 6749 Section 6).
-
#run!(server_url:, resource_metadata_url: nil, scope: nil) ⇒ Object
Runs the full discovery, registration, authorization, and token exchange flow.
Constructor Details
#initialize(provider:, http_client_factory: nil) ⇒ Flow
Returns a new instance of Flow.
26 27 28 29 |
# File 'lib/mcp/client/oauth/flow.rb', line 26 def initialize(provider:, http_client_factory: nil) @provider = provider @http_client_factory = http_client_factory || -> { default_http_client } end |
Instance Method Details
#refresh!(server_url:, resource_metadata_url: nil) ⇒ Object
Exchanges the saved ‘refresh_token` for a fresh access token (RFC 6749 Section 6). Re-discovers PRM and AS metadata so we always pick up a moved token endpoint, and re-runs the audience / issuer / security checks before talking to it.
Returns ‘:refreshed` on success. Raises `AuthorizationError` when the provider has no refresh token, no client information, or when the token endpoint refuses the refresh request. www.rfc-editor.org/rfc/rfc6749#section-6
103 104 105 106 107 108 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 |
# File 'lib/mcp/client/oauth/flow.rb', line 103 def refresh!(server_url:, resource_metadata_url: nil) refresh_token = read_token("refresh_token") raise AuthorizationError, "Cannot refresh: no refresh_token in provider storage." unless refresh_token client_info = @provider.client_information unless client_info.is_a?(Hash) && client_info_required_value(client_info, "client_id") raise AuthorizationError, "Cannot refresh: no client_information in provider storage." end if ensure_secure_url!(, label: "WWW-Authenticate resource_metadata URL") end prm = ( server_url: server_url, resource_metadata_url: , ) = (prm) ensure_secure_url!(, label: "PRM `authorization_servers` entry") resource = canonical_resource(server_url: server_url, prm_resource: prm["resource"]) = (issuer_url: ) ensure_issuer_matches!(expected: , returned: ["issuer"]) ensure_secure_endpoints!() new_tokens = exchange_refresh_token( as_metadata: , client_info: client_info, refresh_token: refresh_token, resource: resource, ) @provider.save_tokens(preserve_refresh_token(new_tokens, refresh_token)) :refreshed end |
#run!(server_url:, resource_metadata_url: nil, scope: nil) ⇒ Object
Runs the full discovery, registration, authorization, and token exchange flow. On success, persists tokens via the provider and returns ‘:authorized`.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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 |
# File 'lib/mcp/client/oauth/flow.rb', line 33 def run!(server_url:, resource_metadata_url: nil, scope: nil) # The `resource_metadata` URL ships in `WWW-Authenticate` and is the very # first thing we contact in the OAuth flow, so it has to clear the same # Communication Security bar as the OAuth endpoints downstream. if ensure_secure_url!(, label: "WWW-Authenticate resource_metadata URL") end prm = ( server_url: server_url, resource_metadata_url: , ) = (prm) ensure_secure_url!(, label: "PRM `authorization_servers` entry") # Per RFC 8707 + MCP authorization, the canonical MCP server URI is sent on # both the authorization and token requests. When PRM advertises a `resource`, # it MUST identify the same MCP server we are talking to; otherwise we are # being redirected to credentials minted for a different audience. resource = canonical_resource(server_url: server_url, prm_resource: prm["resource"]) = (issuer_url: ) ensure_issuer_matches!(expected: , returned: ["issuer"]) ensure_secure_endpoints!() ensure_pkce_supported!() client_info = ensure_client_registered(as_metadata: ) effective_scope = resolve_scope(scope: scope, prm: prm) pkce = PKCE.generate state = SecureRandom.urlsafe_base64(32) = ( as_metadata: , client_id: client_info_required_value(client_info, "client_id"), scope: effective_scope, state: state, code_challenge: pkce[:code_challenge], resource: resource, ) @provider.redirect_handler.call() code, returned_state = Array(@provider.callback_handler.call) raise AuthorizationError, "Authorization callback did not return an authorization code." unless code unless states_match?(returned_state, state) raise AuthorizationError, "OAuth state mismatch (CSRF protection)." end tokens = ( as_metadata: , client_info: client_info, code: code, code_verifier: pkce[:code_verifier], resource: resource, ) @provider.save_tokens(tokens) :authorized end |