Module: McpToolkit::Oauth::ControllerMethods
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/mcp_toolkit/oauth/controller_methods.rb
Overview
The AUTHORITY-side OAuth 2.1 authorization bridge (routes: config/routes.rb; setup + rationale: README).
NOT an identity provider, and reading it as a half-built one will mislead. It
mints no credential, stores no client, models no consent, issues no refresh
token. It is a standards-shaped envelope around tokens the host ALREADY issues
by its own means, for clients that will only authenticate by discovering an
authorization server and running a browser flow: the page asks an operator to
paste a token they hold, and the access_token returned IS that token, verified
through the same config.token_authenticator the transport uses. Scopes,
expiry, revocation and tenancy stay with the host; nothing here widens reach.
So the stubs are deliberate, not unfinished: no endpoint reads the client_id
it hands out (a public client's identifier is self-asserted and gates nothing);
pasting a token you already hold IS the grant; and the pasted token's own expiry
is the real lifetime, so a client re-runs the flow rather than refreshing a
shadow of it.
Two things are NOT mocked, because faking them would be a vulnerability rather
than a skipped ceremony: redirect_uri is checked against the host's policy on
BOTH legs (an unvetted REMOTE target is an open redirect handing out
authorization codes — see Configuration#oauth_allowed_redirect_uris for the
attack it stops), and the PKCE code_verifier is verified.
Constant Summary collapse
- CODE_CACHE_PREFIX =
"mcp_toolkit:oauth:code:"- CODE_BYTES =
32- RESPONSE_OWNED_QUERY_KEYS =
Query parameters the callback response owns: whatever a client put in its own redirect_uri, these are set by the redirect and not carried over from it.
%w[code state].freeze
- PKCE_VALUE =
RFC 7636 §4.1: 43–128 unreserved characters. The challenge is §4.2's base64url of a SHA-256, which is always exactly 43 of the same alphabet.
/\A[A-Za-z0-9\-._~]{43,128}\z/
Instance Method Summary collapse
-
#approve ⇒ Object
The token is verified here, not only at exchange, so a typo fails on the page the operator is looking at.
-
#authorization_server ⇒ Object
S256 because clients send a
code_challengeregardless;nonebecause the clients here are public and unverified. -
#authorize ⇒ Object
formats: [:html]because there is only an HTML template andAcceptpicks the format just as a.jsonsuffix would — without it,Accept: application/jsonraises MissingTemplate on an unauthenticated endpoint. -
#protected_resource ⇒ Object
resourceMUST equal the MCP endpoint URL as the operator typed it into the client, hence derived from the live request origin rather than pinned. -
#register ⇒ Object
Stateless: persisting a
client_idnothing reads would only grow a table of strings the bridge never consults. - #token ⇒ Object
Instance Method Details
#approve ⇒ Object
The token is verified here, not only at exchange, so a typo fails on the page the operator is looking at.
116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/mcp_toolkit/oauth/controller_methods.rb', line 116 def approve access_token = params[:access_token].to_s return mcp_oauth_reject_paste if mcp_oauth_authenticate(access_token).nil? # 303, not Rails' default 302: this POST carried the operator's token in its # body, and only 303 unambiguously tells the browser to fetch the redirect # target with GET and no body. A 302 leaves re-sending it to the client's # discretion, which would hand the token itself to the callback (RFC 9700 # §4.12). redirect_to mcp_oauth_callback_url(mcp_oauth_issue_code(access_token)), allow_other_host: true, status: :see_other end |
#authorization_server ⇒ Object
S256 because clients send a code_challenge regardless; none because the
clients here are public and unverified.
82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/mcp_toolkit/oauth/controller_methods.rb', line 82 def mcp_oauth_forbid_caching render json: { issuer: mcp_oauth_issuer, authorization_endpoint: mcp_oauth_endpoint_url("authorize"), token_endpoint: mcp_oauth_endpoint_url("token"), registration_endpoint: mcp_oauth_endpoint_url("register"), response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"] } end |
#authorize ⇒ Object
formats: [:html] because there is only an HTML template and Accept picks
the format just as a .json suffix would — without it, Accept: application/json raises MissingTemplate on an unauthenticated endpoint.
110 111 112 |
# File 'lib/mcp_toolkit/oauth/controller_methods.rb', line 110 def render :authorize, layout: false, formats: [:html] end |
#protected_resource ⇒ Object
resource MUST equal the MCP endpoint URL as the operator typed it into the
client, hence derived from the live request origin rather than pinned.
71 72 73 74 75 76 77 78 |
# File 'lib/mcp_toolkit/oauth/controller_methods.rb', line 71 def protected_resource mcp_oauth_forbid_caching render json: { resource: mcp_oauth_resource_url, authorization_servers: [mcp_oauth_issuer], bearer_methods_supported: ["header"] } end |
#register ⇒ Object
Stateless: persisting a client_id nothing reads would only grow a table of
strings the bridge never consults.
98 99 100 101 102 103 104 105 |
# File 'lib/mcp_toolkit/oauth/controller_methods.rb', line 98 def register render json: { client_id: SecureRandom.uuid, token_endpoint_auth_method: "none", grant_types: ["authorization_code"], response_types: ["code"] }, status: :created end |
#token ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/mcp_toolkit/oauth/controller_methods.rb', line 129 def token return mcp_oauth_render_token_error("unsupported_grant_type") unless params[:grant_type] == "authorization_code" # Shape first, and BEFORE the code is consumed: a request that could never # verify shouldn't cost a legitimate client its code. A well-formed but WRONG # verifier still burns it below — that is deliberate, and the only thing # stopping someone who intercepted a code from retrying verifiers against it. return mcp_oauth_render_token_error("invalid_request") unless mcp_oauth_verifier_well_formed? payload = mcp_oauth_consume_code(params[:code].to_s) return mcp_oauth_render_token_error("invalid_grant") if payload.nil? return mcp_oauth_render_token_error("invalid_grant") unless mcp_oauth_exchange_valid?(payload) access_token = payload[:access_token].to_s return mcp_oauth_render_token_error("invalid_grant") if mcp_oauth_authenticate(access_token).nil? mcp_oauth_forbid_caching render json: { access_token:, token_type: "Bearer" } end |