Module: BetterAuth::Plugins::MCP
- Defined in:
- lib/better_auth/plugins/mcp.rb,
lib/better_auth/plugins/mcp/token.rb,
lib/better_auth/plugins/mcp/config.rb,
lib/better_auth/plugins/mcp/schema.rb,
lib/better_auth/plugins/mcp/consent.rb,
lib/better_auth/plugins/mcp/metadata.rb,
lib/better_auth/plugins/mcp/userinfo.rb,
lib/better_auth/plugins/mcp/registration.rb,
lib/better_auth/plugins/mcp/authorization.rb,
lib/better_auth/plugins/mcp/legacy_aliases.rb,
lib/better_auth/plugins/mcp/resource_handler.rb
Defined Under Namespace
Modules: ResourceHandler
Constant Summary collapse
- DEFAULT_SCOPES =
%w[openid profile email offline_access].freeze
- DEFAULT_GRANT_TYPES =
[OAuthProtocol::AUTH_CODE_GRANT, OAuthProtocol::REFRESH_GRANT, OAuthProtocol::CLIENT_CREDENTIALS_GRANT].freeze
Class Method Summary collapse
- .authorization_redirect_uri(ctx, config, query, session) ⇒ Object
- .authorize(ctx, config) ⇒ Object
- .consent(ctx, config) ⇒ Object
- .inactive_token_response ⇒ Object
- .introspect(ctx, config) ⇒ Object
- .jwks(ctx, config) ⇒ Object
- .legacy_authorize_endpoint(config) ⇒ Object
- .legacy_jwks_endpoint(config) ⇒ Object
- .legacy_register_endpoint(config) ⇒ Object
- .legacy_token_endpoint(config) ⇒ Object
- .legacy_userinfo_endpoint(config) ⇒ Object
- .mcp_jwks_uri(ctx, config) ⇒ Object
- .mcp_signing_algs(ctx, config) ⇒ Object
- .no_store_headers ⇒ Object
- .normalize_config(options) ⇒ Object
- .oauth_metadata(ctx, config) ⇒ Object
- .parse_login_prompt(value) ⇒ Object
- .prompt_without_login(value) ⇒ Object
- .protected_resource_metadata(ctx, config) ⇒ Object
- .redirect_with_code(ctx, config, query, session) ⇒ Object
- .register_client(ctx, config) ⇒ Object
- .restore_login_prompt(ctx, config) ⇒ Object
- .revoke(ctx, config) ⇒ Object
- .schema ⇒ Object
- .session_from_token(ctx, config) ⇒ Object
- .set_cors_headers(ctx) ⇒ Object
- .token(ctx, config) ⇒ Object
- .userinfo(ctx, config) ⇒ Object
- .validate_issuer_url(value) ⇒ Object
- .validate_resource!(config, body) ⇒ Object
- .with_mcp_auth(app, resource_metadata_url:, auth: nil, resource_metadata_mappings: {}) ⇒ Object
Class Method Details
.authorization_redirect_uri(ctx, config, query, session) ⇒ Object
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 93 94 95 |
# File 'lib/better_auth/plugins/mcp/authorization.rb', line 40 def (ctx, config, query, session) query = OAuthProtocol.stringify_keys(query) prompts = OAuthProtocol.parse_scopes(query["prompt"]) raise ctx.redirect("#{ctx.context.base_url}/error?error=invalid_client") if query["client_id"].to_s.empty? unless query["response_type"] raise ctx.redirect(OAuthProtocol.redirect_uri_with_params(ctx.context.base_url + "/error", error: "invalid_request", error_description: "response_type is required")) end client = OAuthProtocol.find_client(ctx, "oauthClient", query["client_id"]) raise ctx.redirect("#{ctx.context.base_url}/error?error=invalid_client") unless client OAuthProtocol.validate_redirect_uri!(client, query["redirect_uri"]) client_data = OAuthProtocol.stringify_keys(client) raise ctx.redirect("#{ctx.context.base_url}/error?error=client_disabled") if client_data["disabled"] raise ctx.redirect("#{ctx.context.base_url}/error?error=unsupported_response_type") unless query["response_type"] == "code" scopes = OAuthProtocol.parse_scopes(query["scope"] || "openid") allowed_scopes = OAuthProtocol.parse_scopes(client_data["scopes"]) allowed_scopes = OAuthProtocol.parse_scopes(config[:scopes]) if allowed_scopes.empty? invalid_scopes = scopes.reject { |scope| config[:scopes].include?(scope) && allowed_scopes.include?(scope) } unless invalid_scopes.empty? raise ctx.redirect(OAuthProtocol.redirect_uri_with_params(query["redirect_uri"], error: "invalid_scope", error_description: "The following scopes are invalid: #{invalid_scopes.join(", ")}", state: query["state"])) end pkce_error = OAuthProtocol.(client_data, scopes, query["code_challenge"], query["code_challenge_method"]) if pkce_error description = (pkce_error == "PKCE is required") ? "pkce is required" : pkce_error raise ctx.redirect(OAuthProtocol.redirect_uri_with_params(query["redirect_uri"], error: "invalid_request", error_description: description, state: query["state"])) end if prompts.include?("consent") = Crypto.random_string(32) config[:store][:consents][] = { query: query, session: session, client: client, scopes: scopes, expires_at: Time.now + config[:code_expires_in].to_i } raise ctx.redirect(OAuthProtocol.redirect_uri_with_params(config[:consent_page], consent_code: , client_id: client_data["clientId"], scope: OAuthProtocol.scope_string(scopes))) end code = Crypto.random_string(32) OAuthProtocol.store_code( config[:store], code: code, client_id: query["client_id"], redirect_uri: query["redirect_uri"], session: session, scopes: scopes, code_challenge: query["code_challenge"], code_challenge_method: query["code_challenge_method"], nonce: query["nonce"], reference_id: client_data["referenceId"] ) OAuthProtocol.redirect_uri_with_params(query["redirect_uri"], code: code, state: query["state"], iss: validate_issuer_url(OAuthProtocol.issuer(ctx))) end |
.authorize(ctx, config) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/better_auth/plugins/mcp/authorization.rb', line 8 def (ctx, config) set_cors_headers(ctx) query = OAuthProtocol.stringify_keys(ctx.query) session = Routes.current_session(ctx, allow_nil: true) unless session ctx.("oidc_login_prompt", JSON.generate(query), ctx.context.secret, max_age: 600, path: "/", same_site: "lax") raise ctx.redirect(OAuthProtocol.redirect_uri_with_params(config[:login_page], query)) end redirect_with_code(ctx, config, query, session) end |
.consent(ctx, config) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/better_auth/plugins/mcp/consent.rb', line 8 def (ctx, config) current_session = Routes.current_session(ctx) body = OAuthProtocol.stringify_keys(ctx.body) pending = config[:store][:consents].delete(body["consent_code"].to_s) raise APIError.new("BAD_REQUEST", message: "invalid consent_code") unless pending raise APIError.new("BAD_REQUEST", message: "expired consent_code") if pending[:expires_at] <= Time.now query = pending[:query] if body["accept"] == false || body["accept"].to_s == "false" return {redirectURI: OAuthProtocol.redirect_uri_with_params(query["redirect_uri"], error: "access_denied", state: query["state"], iss: validate_issuer_url(OAuthProtocol.issuer(ctx)))} end granted_scopes = OAuthProtocol.parse_scopes(body["scope"] || body["scopes"]) granted_scopes = pending[:scopes] if granted_scopes.empty? unless granted_scopes.all? { |scope| pending[:scopes].include?(scope) } raise APIError.new("BAD_REQUEST", message: "invalid_scope") end pending[:session] = current_session if current_session query = query.merge("scope" => OAuthProtocol.scope_string(granted_scopes)).except("prompt") {redirectURI: (ctx, config, query, pending[:session])} end |
.inactive_token_response ⇒ Object
103 104 105 |
# File 'lib/better_auth/plugins/mcp/token.rb', line 103 def inactive_token_response {active: false} end |
.introspect(ctx, config) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/better_auth/plugins/mcp/token.rb', line 75 def introspect(ctx, config) OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix]) body = OAuthProtocol.stringify_keys(ctx.body) token_record = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix]) return inactive_token_response if token_record.nil? || token_record["revoked"] || (token_record["expiresAt"] && token_record["expiresAt"] <= Time.now) { active: true, client_id: token_record["clientId"], scope: OAuthProtocol.scope_string(token_record["scope"] || token_record["scopes"]), sub: token_record["subject"] || token_record.dig("user", "id"), iss: token_record["issuer"], iat: token_record["issuedAt"]&.to_i, exp: token_record["expiresAt"]&.to_i, sid: token_record["sessionId"], aud: token_record["audience"] }.compact end |
.jwks(ctx, config) ⇒ Object
74 75 76 77 78 |
# File 'lib/better_auth/plugins/mcp/metadata.rb', line 74 def jwks(ctx, config) jwt_config = config[:jwt] || {} BetterAuth::Plugins.create_jwk(ctx, jwt_config) if BetterAuth::Plugins.all_jwks(ctx, jwt_config).empty? {keys: BetterAuth::Plugins.public_jwks(ctx, jwt_config).map { |key| BetterAuth::Plugins.public_jwk(key, jwt_config) }} end |
.legacy_authorize_endpoint(config) ⇒ Object
14 15 16 17 18 |
# File 'lib/better_auth/plugins/mcp/legacy_aliases.rb', line 14 def (config) Endpoint.new(path: "/mcp/authorize", method: "GET") do |ctx| (ctx, config) end end |
.legacy_jwks_endpoint(config) ⇒ Object
32 33 34 35 36 |
# File 'lib/better_auth/plugins/mcp/legacy_aliases.rb', line 32 def legacy_jwks_endpoint(config) Endpoint.new(path: "/mcp/jwks", method: "GET") do |ctx| ctx.json(jwks(ctx, config)) end end |
.legacy_register_endpoint(config) ⇒ Object
8 9 10 11 12 |
# File 'lib/better_auth/plugins/mcp/legacy_aliases.rb', line 8 def legacy_register_endpoint(config) Endpoint.new(path: "/mcp/register", method: "POST") do |ctx| ctx.json(register_client(ctx, config), status: 201, headers: no_store_headers) end end |
.legacy_token_endpoint(config) ⇒ Object
20 21 22 23 24 |
# File 'lib/better_auth/plugins/mcp/legacy_aliases.rb', line 20 def legacy_token_endpoint(config) Endpoint.new(path: "/mcp/token", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx| ctx.json(token(ctx, config), headers: no_store_headers) end end |
.legacy_userinfo_endpoint(config) ⇒ Object
26 27 28 29 30 |
# File 'lib/better_auth/plugins/mcp/legacy_aliases.rb', line 26 def legacy_userinfo_endpoint(config) Endpoint.new(path: "/mcp/userinfo", method: "GET") do |ctx| ctx.json(userinfo(ctx, config)) end end |
.mcp_jwks_uri(ctx, config) ⇒ Object
61 62 63 64 65 |
# File 'lib/better_auth/plugins/mcp/metadata.rb', line 61 def mcp_jwks_uri(ctx, config) config.dig(:oidc_config, :metadata, :jwks_uri) || config.dig(:advertised_metadata, :jwks_uri) || "#{OAuthProtocol.endpoint_base(ctx)}/oauth2/jwks" end |
.mcp_signing_algs(ctx, config) ⇒ Object
67 68 69 70 71 72 |
# File 'lib/better_auth/plugins/mcp/metadata.rb', line 67 def mcp_signing_algs(ctx, config) jwt_plugin = ctx.context..plugins.find { |plugin| plugin.id == "jwt" } alg = config.dig(:jwt, :jwks, :key_pair_config, :alg) || jwt_plugin&.&.dig(:jwks, :key_pair_config, :alg) [alg || "EdDSA"] end |
.no_store_headers ⇒ Object
46 47 48 |
# File 'lib/better_auth/plugins/mcp/config.rb', line 46 def no_store_headers {"Cache-Control" => "no-store", "Pragma" => "no-cache"} end |
.normalize_config(options) ⇒ Object
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/better_auth/plugins/mcp/config.rb', line 11 def normalize_config() incoming = BetterAuth::Plugins.normalize_hash( || {}) oidc = BetterAuth::Plugins.normalize_hash(incoming[:oidc_config] || {}) base = { login_page: "/login", consent_page: "/oauth2/consent", resource: nil, scopes: DEFAULT_SCOPES, grant_types: DEFAULT_GRANT_TYPES, allow_dynamic_client_registration: true, allow_unauthenticated_client_registration: true, require_pkce: true, code_expires_in: 600, access_token_expires_in: 3600, refresh_token_expires_in: 604_800, m2m_access_token_expires_in: 3600, store_client_secret: "plain", prefix: {}, store: OAuthProtocol.stores } config = base.merge(oidc.except(:metadata)).merge(incoming) config[:oidc_config] = oidc config[:scopes] = (Array(base[:scopes]) + Array(oidc[:scopes]) + Array(incoming[:scopes])).compact.map(&:to_s).uniq config[:grant_types] = Array(config[:grant_types]).map(&:to_s) config[:prefix] = BetterAuth::Plugins.normalize_hash(config[:prefix] || {}) config end |
.oauth_metadata(ctx, config) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/better_auth/plugins/mcp/metadata.rb', line 22 def (ctx, config) base = OAuthProtocol.endpoint_base(ctx) { issuer: validate_issuer_url(OAuthProtocol.issuer(ctx)), authorization_endpoint: "#{base}/oauth2/authorize", token_endpoint: "#{base}/oauth2/token", userinfo_endpoint: "#{base}/oauth2/userinfo", registration_endpoint: "#{base}/oauth2/register", introspection_endpoint: "#{base}/oauth2/introspect", revocation_endpoint: "#{base}/oauth2/revoke", jwks_uri: mcp_jwks_uri(ctx, config), scopes_supported: config[:scopes], response_types_supported: ["code"], response_modes_supported: ["query"], grant_types_supported: config[:grant_types], subject_types_supported: ["public"], id_token_signing_alg_values_supported: mcp_signing_algs(ctx, config), token_endpoint_auth_methods_supported: ["none", "client_secret_basic", "client_secret_post"], introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"], revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"], code_challenge_methods_supported: ["S256"], authorization_response_iss_parameter_supported: true, claims_supported: %w[sub iss aud exp iat sid scope azp email email_verified name picture family_name given_name] }.merge(BetterAuth::Plugins.normalize_hash(config.dig(:oidc_config, :metadata) || {})) end |
.parse_login_prompt(value) ⇒ Object
103 104 105 106 107 108 |
# File 'lib/better_auth/plugins/mcp/authorization.rb', line 103 def parse_login_prompt(value) parsed = JSON.parse(value.to_s) parsed.is_a?(Hash) ? parsed : nil rescue JSON::ParserError nil end |
.prompt_without_login(value) ⇒ Object
97 98 99 100 101 |
# File 'lib/better_auth/plugins/mcp/authorization.rb', line 97 def prompt_without_login(value) prompts = OAuthProtocol.parse_scopes(value) prompts.delete("login") OAuthProtocol.scope_string(prompts) end |
.protected_resource_metadata(ctx, config) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/better_auth/plugins/mcp/metadata.rb', line 48 def (ctx, config) base = OAuthProtocol.endpoint_base(ctx) origin = OAuthProtocol.origin_for(base) { resource: config[:resource] || origin, authorization_servers: [origin], jwks_uri: mcp_jwks_uri(ctx, config), scopes_supported: config[:scopes], bearer_methods_supported: ["header"], resource_signing_alg_values_supported: mcp_signing_algs(ctx, config) } end |
.redirect_with_code(ctx, config, query, session) ⇒ Object
36 37 38 |
# File 'lib/better_auth/plugins/mcp/authorization.rb', line 36 def redirect_with_code(ctx, config, query, session) raise ctx.redirect((ctx, config, query, session)) end |
.register_client(ctx, config) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/better_auth/plugins/mcp/registration.rb', line 8 def register_client(ctx, config) set_cors_headers(ctx) body = OAuthProtocol.stringify_keys(ctx.body) body["token_endpoint_auth_method"] ||= "none" body["grant_types"] ||= [OAuthProtocol::AUTH_CODE_GRANT, OAuthProtocol::REFRESH_GRANT] body["response_types"] ||= ["code"] body["require_pkce"] = true unless body.key?("require_pkce") || body.key?("requirePKCE") OAuthProtocol.create_client( ctx, model: "oauthClient", body: body, default_auth_method: "none", store_client_secret: config[:store_client_secret], default_scopes: config[:scopes], allowed_scopes: config[:scopes], prefix: config[:prefix], dynamic_registration: true, strip_client_metadata: true ) end |
.restore_login_prompt(ctx, config) ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/better_auth/plugins/mcp/authorization.rb', line 20 def restore_login_prompt(ctx, config) = ctx.("oidc_login_prompt", ctx.context.secret) return unless session = ctx.context.new_session return unless session && session[:session] && ctx.response_headers["set-cookie"].to_s.include?(ctx.context.[:session_token].name) query = parse_login_prompt() return unless query query["prompt"] = prompt_without_login(query["prompt"]) if query.key?("prompt") ctx.("oidc_login_prompt", "", path: "/", max_age: 0) ctx.context.set_current_session(session) if ctx.context.respond_to?(:set_current_session) [302, ctx.response_headers.merge("location" => (ctx, config, query, session)), [""]] end |
.revoke(ctx, config) ⇒ Object
94 95 96 97 98 99 100 101 |
# File 'lib/better_auth/plugins/mcp/token.rb', line 94 def revoke(ctx, config) OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix]) body = OAuthProtocol.stringify_keys(ctx.body) if (token_record = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix])) token_record["revoked"] = Time.now end {revoked: true} end |
.schema ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 |
# File 'lib/better_auth/plugins/mcp/schema.rb', line 8 def schema { oauthClient: { modelName: "oauthClient", fields: { clientId: {type: "string", unique: true, required: true}, clientSecret: {type: "string", required: false}, disabled: {type: "boolean", default_value: false, required: false}, skipConsent: {type: "boolean", required: false}, enableEndSession: {type: "boolean", required: false}, clientSecretExpiresAt: {type: "number", required: false}, scopes: {type: "string[]", required: false}, userId: {type: "string", required: false}, createdAt: {type: "date", required: true, default_value: -> { Time.now }}, updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }}, name: {type: "string", required: false}, uri: {type: "string", required: false}, icon: {type: "string", required: false}, contacts: {type: "string[]", required: false}, tos: {type: "string", required: false}, policy: {type: "string", required: false}, softwareId: {type: "string", required: false}, softwareVersion: {type: "string", required: false}, softwareStatement: {type: "string", required: false}, redirectUris: {type: "string[]", required: true}, postLogoutRedirectUris: {type: "string[]", required: false}, tokenEndpointAuthMethod: {type: "string", required: false}, grantTypes: {type: "string[]", required: false}, responseTypes: {type: "string[]", required: false}, public: {type: "boolean", required: false}, type: {type: "string", required: false}, requirePKCE: {type: "boolean", required: false}, subjectType: {type: "string", required: false}, referenceId: {type: "string", required: false}, metadata: {type: "json", required: false} } }, oauthRefreshToken: { fields: { token: {type: "string", required: true}, clientId: {type: "string", required: true}, sessionId: {type: "string", required: false}, userId: {type: "string", required: false}, referenceId: {type: "string", required: false}, authTime: {type: "date", required: false}, expiresAt: {type: "date", required: false}, createdAt: {type: "date", required: true, default_value: -> { Time.now }}, revoked: {type: "date", required: false}, scopes: {type: "string[]", required: true} } }, oauthAccessToken: { modelName: "oauthAccessToken", fields: { token: {type: "string", unique: true, required: true}, expiresAt: {type: "date", required: true}, clientId: {type: "string", required: true}, userId: {type: "string", required: false}, sessionId: {type: "string", required: false}, scopes: {type: "string[]", required: true}, revoked: {type: "date", required: false}, referenceId: {type: "string", required: false}, authTime: {type: "date", required: false}, refreshId: {type: "string", required: false}, createdAt: {type: "date", required: true, default_value: -> { Time.now }}, updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }} } }, oauthConsent: { modelName: "oauthConsent", fields: { clientId: {type: "string", required: true}, userId: {type: "string", required: false}, referenceId: {type: "string", required: false}, scopes: {type: "string[]", required: true}, createdAt: {type: "date", required: true, default_value: -> { Time.now }}, updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }} } } } end |
.session_from_token(ctx, config) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/better_auth/plugins/mcp/userinfo.rb', line 14 def session_from_token(ctx, config) = ctx.headers["authorization"].to_s token_value = .start_with?("Bearer ") ? .delete_prefix("Bearer ").strip : .strip return nil if token_value.empty? token_record = OAuthProtocol.token_record(config[:store], token_value, prefix: config[:prefix]) return token_record if token_record payload = ::JWT.decode(token_value, ctx.context.secret, true, algorithm: "HS256").first { "clientId" => payload["azp"], "userId" => payload["sub"], "sessionId" => payload["sid"], "scopes" => OAuthProtocol.parse_scopes(payload["scope"]), "audience" => payload["aud"], "subject" => payload["sub"], "expiresAt" => payload["exp"] ? Time.at(payload["exp"].to_i) : nil }.compact rescue ::JWT::DecodeError nil end |
.set_cors_headers(ctx) ⇒ Object
39 40 41 42 43 44 |
# File 'lib/better_auth/plugins/mcp/config.rb', line 39 def set_cors_headers(ctx) ctx.set_header("Access-Control-Allow-Origin", "*") ctx.set_header("Access-Control-Allow-Methods", "POST, OPTIONS") ctx.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization") ctx.set_header("Access-Control-Max-Age", "86400") end |
.token(ctx, config) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 |
# File 'lib/better_auth/plugins/mcp/token.rb', line 8 def token(ctx, config) set_cors_headers(ctx) body = OAuthProtocol.stringify_keys(ctx.body) client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix]) audience = validate_resource!(config, body) case body["grant_type"] when OAuthProtocol::AUTH_CODE_GRANT code = OAuthProtocol.consume_code!( config[:store], body["code"], client_id: OAuthProtocol.stringify_keys(client)["clientId"], redirect_uri: body["redirect_uri"], code_verifier: body["code_verifier"] ) OAuthProtocol.issue_tokens( ctx, config[:store], model: "oauthAccessToken", client: client, session: code[:session], scopes: code[:scopes], include_refresh: code[:scopes].include?("offline_access"), issuer: validate_issuer_url(OAuthProtocol.issuer(ctx)), prefix: config[:prefix], refresh_token_expires_in: config[:refresh_token_expires_in], access_token_expires_in: config[:access_token_expires_in], audience: audience, grant_type: OAuthProtocol::AUTH_CODE_GRANT, jwt_access_token: !audience.nil?, nonce: code[:nonce], auth_time: code[:auth_time], reference_id: code[:reference_id], filter_id_token_claims_by_scope: true ) when OAuthProtocol::REFRESH_GRANT OAuthProtocol.refresh_tokens( ctx, config[:store], model: "oauthAccessToken", client: client, refresh_token: body["refresh_token"], scopes: body["scope"], issuer: validate_issuer_url(OAuthProtocol.issuer(ctx)), prefix: config[:prefix], refresh_token_expires_in: config[:refresh_token_expires_in], access_token_expires_in: config[:access_token_expires_in], audience: audience, jwt_access_token: !audience.nil?, filter_id_token_claims_by_scope: true ) else raise APIError.new("BAD_REQUEST", message: "unsupported_grant_type") end end |
.userinfo(ctx, config) ⇒ Object
10 11 12 |
# File 'lib/better_auth/plugins/mcp/userinfo.rb', line 10 def userinfo(ctx, config) OAuthProtocol.userinfo(config[:store], ctx.headers["authorization"], prefix: config[:prefix], jwt_secret: ctx.context.secret) end |
.validate_issuer_url(value) ⇒ Object
10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/better_auth/plugins/mcp/metadata.rb', line 10 def validate_issuer_url(value) uri = URI.parse(value.to_s) uri.query = nil uri.fragment = nil if uri.scheme == "http" && !["localhost", "127.0.0.1", "::1"].include?(uri.hostname || uri.host) uri.scheme = "https" end uri.to_s.sub(%r{/+\z}, "") rescue URI::InvalidURIError value.to_s.split(/[?#]/).first.sub(%r{/+\z}, "") end |
.validate_resource!(config, body) ⇒ Object
64 65 66 67 68 69 70 71 72 73 |
# File 'lib/better_auth/plugins/mcp/token.rb', line 64 def validate_resource!(config, body) resources = Array(body["resource"]).compact.map(&:to_s) return nil if resources.empty? valid = Array(config[:valid_audiences]).map(&:to_s) resources.each do |resource| raise APIError.new("BAD_REQUEST", message: "requested resource invalid") unless valid.empty? || valid.include?(resource) end (resources.length == 1) ? resources.first : resources end |
.with_mcp_auth(app, resource_metadata_url:, auth: nil, resource_metadata_mappings: {}) ⇒ Object
20 21 22 23 24 25 26 27 |
# File 'lib/better_auth/plugins/mcp.rb', line 20 def with_mcp_auth(app, resource_metadata_url:, auth: nil, resource_metadata_mappings: {}) ResourceHandler.with_mcp_auth( app, resource_metadata_url: , auth: auth, resource_metadata_mappings: ) end |